// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#ifdef FEATURE_INTERPRETER

#include "threads.h"
#include "gcenv.h"
#include "interpexec.h"
#include "frames.h"
#include "virtualcallstub.h"
#include "comdelegate.h"
#include "gchelpers.inl"
#include "arraynative.inl"

// for numeric_limits
#include <limits>
#include <functional>

#ifdef TARGET_WASM
// Unused on WASM
#define SAVE_THE_LOWEST_SP do {} while (0)
#else
// Save the lowest SP in the current method so that we can identify it by that during stackwalk
#define SAVE_THE_LOWEST_SP pInterpreterFrame->SetInterpExecMethodSP((TADDR)GetCurrentSP())
#endif // !TARGET_WASM

// Call invoker helpers provided by platform.
void InvokeManagedMethod(MethodDesc *pMD, int8_t *pArgs, int8_t *pRet, PCODE target, Object** pContinuationRet);
void InvokeUnmanagedMethod(MethodDesc *targetMethod, int8_t *pArgs, int8_t *pRet, PCODE callTarget);
void InvokeCalliStub(PCODE ftn, void* cookie, int8_t *pArgs, int8_t *pRet, Object** pContinuationRet);
void InvokeUnmanagedCalli(PCODE ftn, void *cookie, int8_t *pArgs, int8_t *pRet);
void InvokeDelegateInvokeMethod(MethodDesc *pMDDelegateInvoke, int8_t *pArgs, int8_t *pRet, PCODE target, Object** pContinuationRet);
extern "C" PCODE CID_VirtualOpenDelegateDispatch(TransitionBlock * pTransitionBlock);

// Filter to ignore SEH exceptions representing C++ exceptions.
LONG IgnoreCppExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo, PVOID pv)
{
    return (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_MSVC)
        ? EXCEPTION_CONTINUE_SEARCH
        : EXCEPTION_EXECUTE_HANDLER;
}

template<typename Function>
std::invoke_result_t<Function> CallWithSEHWrapper(Function function)
{
    struct Local
    {
        Function function;
        std::invoke_result_t<Function> result;
    } local { function };

    PAL_TRY(Local *, pParam, &local)
    {
        pParam->result = pParam->function();
    }
    PAL_EXCEPT_FILTER(IgnoreCppExceptionFilter)
    {
        // There can be both C++ (thrown by the COMPlusThrow) and managed exceptions thrown
        // from the called method's call chain.
        // We need to process only managed ones here, the C++ ones are handled by the
        // INSTALL_/UNINSTALL_UNWIND_AND_CONTINUE_HANDLER in the InterpExecMethod.
        // The managed ones are represented by SEH exception, which cannot be handled there
        // because it is not possible to handle both SEH and C++ exceptions in the same frame.
        GCX_COOP_NO_DTOR();
        OBJECTREF ohThrowable = GetThread()->LastThrownObject();
        DispatchManagedException(ohThrowable);
    }
    PAL_ENDTRY

    return local.result;
}

// Use the NOINLINE to ensure that the InlinedCallFrame in this method is a lower stack address than any InterpMethodContextFrame values.
NOINLINE
void InvokeUnmanagedMethodWithTransition(MethodDesc *targetMethod, int8_t *stack, InterpMethodContextFrame *pFrame, int8_t *pArgs, int8_t *pRet, PCODE callTarget)
{
    InlinedCallFrame inlinedCallFrame;
    inlinedCallFrame.m_pCallerReturnAddress = (TADDR)pFrame->ip;
    inlinedCallFrame.m_pCallSiteSP = pFrame;
    inlinedCallFrame.m_pCalleeSavedFP = (TADDR)stack;
    inlinedCallFrame.m_pThread = GetThread();
    inlinedCallFrame.m_Datum = NULL;
    inlinedCallFrame.Push();

    struct Param
    {
        MethodDesc *targetMethod;
        int8_t *pArgs;
        int8_t *pRet;
        PCODE callTarget;
    }
    param = { targetMethod, pArgs, pRet, callTarget };

    PAL_TRY(Param *, pParam, &param)
    {
        GCX_PREEMP_NO_DTOR();
        // WASM-TODO: Handle unmanaged calling conventions
        InvokeManagedMethod(pParam->targetMethod, pParam->pArgs, pParam->pRet, pParam->callTarget, NULL);
        GCX_PREEMP_NO_DTOR_END();
    }
    PAL_EXCEPT_FILTER(IgnoreCppExceptionFilter)
    {
        // There can be both C++ (thrown by the COMPlusThrow) and managed exceptions thrown
        // from the native method (QCALL) call chain.
        // We need to process only managed ones here, the C++ ones are handled by the
        // INSTALL_/UNINSTALL_UNWIND_AND_CONTINUE_HANDLER in the InterpExecMethod.
        // The managed ones are represented by SEH exception, which cannot be handled there
        // because it is not possible to handle both SEH and C++ exceptions in the same frame.
        GCX_COOP_NO_DTOR();
        OBJECTREF ohThrowable = GetThread()->LastThrownObject();
        DispatchManagedException(ohThrowable);
    }
    PAL_ENDTRY

    inlinedCallFrame.Pop();
}

NOINLINE
void InvokeUnmanagedCalliWithTransition(PCODE ftn, void *cookie, int8_t *stack, InterpMethodContextFrame *pFrame, int8_t *pArgs, int8_t *pRet)
{
    CONTRACTL
    {
        THROWS;
        MODE_COOPERATIVE;
        PRECONDITION(CheckPointer((void*)ftn));
        PRECONDITION(CheckPointer(cookie));
    }
    CONTRACTL_END

    InlinedCallFrame inlinedCallFrame;
    inlinedCallFrame.m_pCallerReturnAddress = (TADDR)pFrame->ip;
    inlinedCallFrame.m_pCallSiteSP = pFrame;
    inlinedCallFrame.m_pCalleeSavedFP = (TADDR)stack;
    inlinedCallFrame.m_pThread = GetThread();
    inlinedCallFrame.m_Datum = NULL;
    inlinedCallFrame.Push();
    {
        GCX_PREEMP();
        InvokeUnmanagedCalli(ftn, cookie, pArgs, pRet);
    }
    inlinedCallFrame.Pop();
}

#ifndef TARGET_WASM
#include "callstubgenerator.h"

static CallStubHeader *UpdateCallStubForMethod(MethodDesc *pMD, PCODE target)
{
    CONTRACTL
    {
        THROWS;
        MODE_ANY;
        PRECONDITION(CheckPointer(pMD));
    }
    CONTRACTL_END

    GCX_PREEMP();

    CallStubGenerator callStubGenerator;

    AllocMemTracker amTracker;
    CallStubHeader *header = callStubGenerator.GenerateCallStub(pMD, &amTracker, true /* interpreterToNative */);

    if (target != (PCODE)NULL)
    {
        header->SetTarget(target);
    }

    if (pMD->SetCallStub(header))
    {
        amTracker.SuppressRelease();
    }
    else
    {
        // We have lost the race for generating the header, use the one that was generated by another thread
        // and let the amTracker release the memory of the one we generated.
        header = pMD->GetCallStub();
    }

    return header;
}

MethodDesc* GetTargetPInvokeMethodDesc(PCODE target)
{
    CONTRACTL
    {
        THROWS;
        MODE_ANY;
        PRECONDITION(CheckPointer((void*)target));
    }
    CONTRACTL_END

    GCX_PREEMP();

    RangeSection * pRS = ExecutionManager::FindCodeRange(target, ExecutionManager::GetScanFlags());
    if (pRS != NULL && pRS->_flags & RangeSection::RANGE_SECTION_RANGELIST)
    {
        if (pRS->_pRangeList->GetCodeBlockKind() == STUB_CODE_BLOCK_STUBPRECODE)
        {
            if (((StubPrecode*)target)->GetType() == PRECODE_PINVOKE_IMPORT)
            {
                return dac_cast<PTR_MethodDesc>(((PInvokeImportPrecode*)target)->GetMethodDesc());
            }
        }
    }

    return NULL;
}

void InvokeManagedMethod(MethodDesc *pMD, int8_t *pArgs, int8_t *pRet, PCODE target, Object** pContinuationRet)
{
    CONTRACTL
    {
        THROWS;
        MODE_ANY;
        PRECONDITION(CheckPointer(pMD));
        PRECONDITION(CheckPointer(pArgs));
        PRECONDITION(CheckPointer(pRet));
    }
    CONTRACTL_END

    CallStubHeader *pHeader = pMD->GetCallStub();
    if (pHeader == NULL)
    {
        pHeader = UpdateCallStubForMethod(pMD, target == (PCODE)NULL ? pMD->GetMultiCallableAddrOfCode(CORINFO_ACCESS_ANY) : target);
    }

    if (target != (PCODE)NULL)
    {
        PCODE headerTarget = pHeader->GetTarget();
        if (target != headerTarget)
        {
#ifdef DEBUG
            // For pinvokes we may have a PInvokeImportPrecode as a pointer, and need to use passed in target preferentially
            // Since on multiple threads we may be racing to use this method, it is possible that the current target could be
            // the PInvokeImportPrecode or the actual target method, so we should allow either of them to match.
            MethodDesc *pMDTarget = GetTargetPInvokeMethodDesc(target);
            MethodDesc *pMDHeaderTarget = GetTargetPInvokeMethodDesc(headerTarget);

            _ASSERTE(pMDTarget == NULL || pMDHeaderTarget == NULL);
            _ASSERTE(pMDTarget == NULL || pMDTarget == pMD);
            _ASSERTE(pMDHeaderTarget == NULL || pMDHeaderTarget == pMD);
#endif // DEBUG
            pHeader->SetTarget(target);
        }
    }

    Object* continuationUnused = NULL;
    if (!pHeader->HasContinuationRet)
    {
        pContinuationRet = &continuationUnused;
    }

    pHeader->Invoke(pHeader->Routines, pArgs, pRet, pHeader->TotalStackSize, pContinuationRet);
}

void InvokeUnmanagedMethod(MethodDesc *targetMethod, int8_t *pArgs, int8_t *pRet, PCODE callTarget)
{
    InvokeManagedMethod(targetMethod, pArgs, pRet, callTarget, NULL);
}

void InvokeDelegateInvokeMethod(MethodDesc *pMDDelegateInvoke, int8_t *pArgs, int8_t *pRet, PCODE target, Object** pContinuationRet)
{
    CONTRACTL
    {
        THROWS;
        MODE_COOPERATIVE;
        PRECONDITION(CheckPointer(pMDDelegateInvoke));
        PRECONDITION(CheckPointer(pArgs));
        PRECONDITION(CheckPointer(pRet));
    }
    CONTRACTL_END

    CallStubHeader *stubHeaderTemplate = pMDDelegateInvoke->GetCallStub();
    if (stubHeaderTemplate == NULL)
    {
        stubHeaderTemplate = UpdateCallStubForMethod(pMDDelegateInvoke, (PCODE)pMDDelegateInvoke->GetMultiCallableAddrOfCode(CORINFO_ACCESS_ANY));
    }

    // CallStubHeaders encode their destination addresses in the Routines array, so they need to be
    // copied to a local buffer before we can actually set their target address.
    size_t templateSize = stubHeaderTemplate->GetSize();
    uint8_t* actualCallStub = (uint8_t*)alloca(templateSize);
    memcpy(actualCallStub, stubHeaderTemplate, templateSize);
    CallStubHeader *pHeader = (CallStubHeader*)actualCallStub;
    pHeader->SetTarget(target); // The method to call

    Object* continuationUnused = NULL;
    if (!pHeader->HasContinuationRet)
    {
        pContinuationRet = &continuationUnused;
    }

    pHeader->Invoke(pHeader->Routines, pArgs, pRet, pHeader->TotalStackSize, pContinuationRet);
}

void InvokeUnmanagedCalli(PCODE ftn, void *cookie, int8_t *pArgs, int8_t *pRet)
{
    CONTRACTL
    {
        THROWS;
        MODE_ANY;
        PRECONDITION(CheckPointer((void*)ftn));
        PRECONDITION(CheckPointer(cookie));
    }
    CONTRACTL_END

    // CallStubHeaders encode their destination addresses in the Routines array, so they need to be
    // copied to a local buffer before we can actually set their target address.
    CallStubHeader* stubHeaderTemplate = (CallStubHeader*)cookie;
    size_t templateSize = stubHeaderTemplate->GetSize();
    uint8_t* actualCallStub = (uint8_t*)alloca(templateSize);
    memcpy(actualCallStub, stubHeaderTemplate, templateSize);
    CallStubHeader *pHeader = (CallStubHeader*)actualCallStub;
    pHeader->SetTarget(ftn); // The method to call
    Object* continuationUnused = NULL;

    pHeader->Invoke(pHeader->Routines, pArgs, pRet, pHeader->TotalStackSize, &continuationUnused);
}

void InvokeCalliStub(PCODE ftn, void *cookie, int8_t *pArgs, int8_t *pRet, Object** pContinuationRet)
{
    CONTRACTL
    {
        THROWS;
        MODE_ANY;
        PRECONDITION(CheckPointer((void*)ftn));
        PRECONDITION(CheckPointer(cookie));
    }
    CONTRACTL_END

    // CallStubHeaders encode their destination addresses in the Routines array, so they need to be
    // copied to a local buffer before we can actually set their target address.
    CallStubHeader* stubHeaderTemplate = (CallStubHeader*)cookie;
    size_t templateSize = stubHeaderTemplate->GetSize();
    uint8_t* actualCallStub = (uint8_t*)alloca(templateSize);
    memcpy(actualCallStub, stubHeaderTemplate, templateSize);
    CallStubHeader *pHeader = (CallStubHeader*)actualCallStub;
    pHeader->SetTarget(ftn); // The method to call

    Object* continuationUnused = NULL;
    if (!pHeader->HasContinuationRet)
    {
        pContinuationRet = &continuationUnused;
    }

    pHeader->Invoke(pHeader->Routines, pArgs, pRet, pHeader->TotalStackSize, pContinuationRet);
}

void* GetCookieForCalliSig(MetaSig metaSig)
{
    STANDARD_VM_CONTRACT;

    CallStubGenerator callStubGenerator;
    return callStubGenerator.GenerateCallStubForSig(metaSig);
}

// Create call stub for calling interpreted methods from JITted/AOTed code.
CallStubHeader *CreateNativeToInterpreterCallStub(InterpMethod* pInterpMethod)
{
    CallStubGenerator callStubGenerator;
    CallStubHeader *pHeader = VolatileLoadWithoutBarrier(&pInterpMethod->pCallStub);
    if (pHeader != NULL)
    {
        return pHeader;
    }
    GCX_PREEMP();

    AllocMemTracker amTracker;
    pHeader = callStubGenerator.GenerateCallStub(pInterpMethod->methodHnd, &amTracker, false /* interpreterToNative */);

    if (InterlockedCompareExchangeT(&pInterpMethod->pCallStub, pHeader, NULL) == NULL)
    {
        amTracker.SuppressRelease();
    }
    else
    {
        // We have lost the race for generating the header, use the one that was generated by another thread
        // and let the amTracker release the memory of the one we generated.
        pHeader = VolatileLoadWithoutBarrier(&pInterpMethod->pCallStub);
    }

    return pHeader;
}
#endif // !TARGET_WASM

#ifdef _DEBUG
void DBG_PrintInterpreterStack()
{
    Thread* pThread = GetThread();
    if (pThread == NULL)
        return;

    int32_t frameCount = 0;

    // Get the "capital F" frame and start walking.
    for (Frame* pFrame = pThread->GetFrame(); pFrame != FRAME_TOP; pFrame = pFrame->PtrNextFrame())
    {
        fprintf(stderr, "Frame (%s): %p\n", Frame::GetFrameTypeName(pFrame->GetFrameIdentifier()), pFrame);
        if (pFrame->GetFrameIdentifier() != FrameIdentifier::InterpreterFrame)
        {
            fprintf(stderr, "    Skipping %p\n", pFrame);
            continue;
        }

        // Walk the current block of managed frames.
        InterpreterFrame* pInterpFrame = (InterpreterFrame*)pFrame;
        InterpMethodContextFrame* cxtFrame = pInterpFrame->GetTopInterpMethodContextFrame();
        while (cxtFrame != NULL)
        {
            MethodDesc* currentMD = cxtFrame->startIp->Method->methodHnd;

            size_t irOffset = ((size_t)cxtFrame->ip - (size_t)(&cxtFrame->startIp[1])) / sizeof(size_t);
            fprintf(stderr, "%4d) %s::%s, IR_%04zx\n",
                frameCount++,
                currentMD->GetMethodTable()->GetDebugClassName(),
                currentMD->GetName(),
                irOffset);
            cxtFrame = cxtFrame->pParent;
        }
    }
}
#endif // _DEBUG

typedef void* (*HELPER_FTN_P_P)(void*);
typedef void* (*HELPER_FTN_BOX_UNBOX)(MethodTable*, void*);
typedef Object* (*HELPER_FTN_NEWARR)(MethodTable*, intptr_t);
typedef void* (*HELPER_FTN_P_PP)(void*, void*);
typedef void (*HELPER_FTN_V_PPP)(void*, void*, void*);
typedef void* (*HELPER_FTN_P_PPIP)(void*, void*, int32_t, void*);
typedef void (*HELPER_FTN_V_PP)(void*, void*);

InterpThreadContext::InterpThreadContext()
{
    // FIXME VirtualAlloc/mmap with INTERP_STACK_ALIGNMENT alignment
    pStackStart = pStackPointer = (int8_t*)malloc(INTERP_STACK_SIZE);
    pStackEnd = pStackStart + INTERP_STACK_SIZE;
}

InterpThreadContext::~InterpThreadContext()
{
    free(pStackStart);
}

DictionaryEntry GenericHandleWorkerCore(MethodDesc * pMD, MethodTable * pMT, LPVOID signature, DWORD dictionaryIndexAndSlot, Module* pModule);

void* GenericHandleCommon(MethodDesc * pMD, MethodTable * pMT, LPVOID signature)
{
    CONTRACTL
    {
        THROWS;
        GC_TRIGGERS;
        MODE_COOPERATIVE;
    } CONTRACTL_END;
    GCX_PREEMP();
    return GenericHandleWorkerCore(pMD, pMT, signature, 0xFFFFFFFF, NULL);
}

#ifdef DEBUG
static void InterpBreakpoint()
{

}
#endif

#define LOCAL_VAR_ADDR(offset,type) ((type*)(stack + (offset)))
#define LOCAL_VAR(offset,type) (*LOCAL_VAR_ADDR(offset, type))
#define NULL_CHECK(o) do { if ((o) == NULL) { COMPlusThrow(kNullReferenceException); } } while (0)


static OBJECTREF CreateMultiDimArray(MethodTable* arrayClass, int8_t* stack, int32_t dimsOffset, int numArgs)
{
    int32_t* dims = (int32_t*)alloca(numArgs * sizeof(int32_t));
    int8_t* dimsBase = LOCAL_VAR_ADDR(dimsOffset, int8_t);
    for (int i = 0; i < numArgs; i++)
    {
        dims[i] = *(int32_t*)(dimsBase + i * 8);
    }

    return AllocateArrayEx(arrayClass, dims, numArgs);
}

template <typename THelper> static THelper GetPossiblyIndirectHelper(const InterpMethod* pMethod, int32_t _data, MethodDesc** pILTargetMethod = NULL)
{
    InterpHelperData data{};
    memcpy(&data, &_data, sizeof(_data));

    void* addr = pMethod->pDataItems[data.addressDataItemIndex];
    switch (data.accessType)
    {
        case IAT_VALUE:
            break;
        case IAT_PVALUE:
            addr = *(void**)addr;
            break;
        case IAT_PPVALUE:
            addr = **(void***)addr;
            break;
        default:
            COMPlusThrowHR(COR_E_EXECUTIONENGINE);
            break;
    }

#ifdef FEATURE_PORTABLE_ENTRYPOINTS
    if (!PortableEntryPoint::HasNativeEntryPoint((PCODE)addr))
    {
        _ASSERTE(pILTargetMethod != NULL);
        *pILTargetMethod = PortableEntryPoint::GetMethodDesc((PCODE)addr);
        return NULL; // Return null to interpret this entrypoint
    }
    addr = PortableEntryPoint::GetActualCode((PCODE)addr);
#endif // FEATURE_PORTABLE_ENTRYPOINTS

    return (THelper)addr;
}

// At present our behavior for float to int conversions is to perform a saturating conversion down to either 32 or 64 bits
//  and then perform an unchecked truncation from that intermediate size down to the actual result size.
// See https://github.com/dotnet/runtime/issues/116823
template <typename TResult, typename TIntermediate, typename TSource> static void ConvFpHelper(int8_t *stack, const int32_t *ip)
{
    static_assert(!std::numeric_limits<TSource>::is_integer, "ConvFpHelper is only for use on floats and doubles");
    static_assert(sizeof(TIntermediate) >= sizeof(TResult), "Intermediate type must not be smaller than result type");
    static_assert(std::numeric_limits<TResult>::is_integer, "ConvFpHelper is only for use on floats and doubles to be converted to integers");

    // First, promote the source value to double
    double src = LOCAL_VAR(ip[2], TSource),
        minValue = (double)std::numeric_limits<TIntermediate>::lowest(),
        maxValue = (double)std::numeric_limits<TIntermediate>::max();

    // (src != src) checks for NaN, then we check whether the min and max values (as represented by their closest double)
    //  properly bound the source value so that when it is truncated it will be in range
    // We assume that we are in round-towards-zero mode. For NaN we want to return 0, and for out of range values, saturate.
    TResult result;
    if (src != src)
        result = 0;
    else if (src >= maxValue)
        result = (TResult)std::numeric_limits<TIntermediate>::max();
    else if (!std::numeric_limits<TIntermediate>::is_signed && (src <= -1))
        result = 0;
    else if (std::numeric_limits<TIntermediate>::is_signed && (src < minValue))
        result = (TResult)std::numeric_limits<TIntermediate>::lowest();
    else
        result = (TResult)(TIntermediate)src;

    // According to spec, for result types smaller than int32, we store them on the stack as int32
    if (sizeof(TResult) >= 4)
        LOCAL_VAR(ip[1], TResult) = result;
    else
        LOCAL_VAR(ip[1], int32_t) = (int32_t)result;
}

template <typename TResult, typename TSource> static void ConvOvfFpHelper(int8_t *stack, const int32_t *ip)
{
    static_assert(!std::numeric_limits<TSource>::is_integer, "ConvOvfFpHelper is only for use on floats and doubles");
    static_assert(std::numeric_limits<TResult>::is_integer, "ConvOvfFpHelper is only for use on floats and doubles to be converted to integers");
    static_assert(sizeof(TResult) <= 4, "ConvOvfFpHelper's generic version is only for use on results <= 4 bytes in size");

    // First, promote the source value to double
    double src = LOCAL_VAR(ip[2], TSource),
        minValue = (double)std::numeric_limits<TResult>::lowest() - 1,
        maxValue = (double)std::numeric_limits<TResult>::max() + 1;

    // We assume that we are in round-towards-zero mode
    bool inRange = (src > minValue) && (src < maxValue);

    if (!inRange)
        COMPlusThrow(kOverflowException);

    TResult truncated = (TResult)src;
    // According to spec, for result types smaller than int32, we store them on the stack as int32
    LOCAL_VAR(ip[1], int32_t) = (int32_t)truncated;
}

// I64 and U64 versions based on mono_math.h

template <typename TSource> static void ConvOvfFpHelperI64(int8_t *stack, const int32_t *ip)
{
    static_assert(!std::numeric_limits<TSource>::is_integer, "ConvOvfFpHelper is only for use on floats and doubles");

    const double two63 = 2147483648.0 * 4294967296.0;
    // First, promote the source value to double
    double src = LOCAL_VAR(ip[2], TSource),
        // Define the boundary values we need to be between (see System.Math.ConvertToInt64Checked)
        minValue = (-two63 - 0x402),
        maxValue = two63;

    bool inRange = (src > minValue) && (src < maxValue);
    if (!inRange)
        COMPlusThrow(kOverflowException);

    int64_t truncated = (int64_t)src;
    LOCAL_VAR(ip[1], int64_t) = truncated;
}

template <typename TSource> static void ConvOvfFpHelperU64(int8_t *stack, const int32_t *ip)
{
    static_assert(!std::numeric_limits<TSource>::is_integer, "ConvOvfFpHelper is only for use on floats and doubles");

    // First, promote the source value to double
    double src = LOCAL_VAR(ip[2], TSource),
        // Define the boundary values we need to be between (see System.Math.ConvertToUInt64Checked)
        minValue = -1.0,
        maxValue = 4294967296.0 * 4294967296.0;

    bool inRange = (src > minValue) && (src < maxValue);
    if (!inRange)
        COMPlusThrow(kOverflowException);

    uint64_t truncated = (uint64_t)src;
    LOCAL_VAR(ip[1], uint64_t) = truncated;
}

template <typename TResult, typename TSource> void ConvOvfHelper(int8_t *stack, const int32_t *ip)
{
    static_assert(std::numeric_limits<TSource>::is_integer, "ConvOvfHelper is only for use on integers");
    constexpr bool shrinking = sizeof(TResult) < sizeof(TSource);

    TSource src = LOCAL_VAR(ip[2], TSource);
    bool inRange;
    if (shrinking)
    {
        if (std::numeric_limits<TSource>::is_signed)
            inRange = (src >= (TSource)std::numeric_limits<TResult>::lowest()) && (src <= (TSource)std::numeric_limits<TResult>::max());
        else
            inRange = (src <= (TSource)std::numeric_limits<TResult>::max());
    }
    else
    {
        // Growing conversions with the same signedness shouldn't use an ovf opcode.
        static_assert(
            shrinking || (std::numeric_limits<TResult>::is_signed != std::numeric_limits<TSource>::is_signed),
            "ConvOvfHelper only does growing conversions with sign changes"
        );

        if (std::numeric_limits<TResult>::is_signed)
        {
            if (sizeof(TSource) == sizeof(TResult))
            {
                // unsigned -> signed conv with same size. check to make sure the source value isn't too big.
                inRange = src <= (TSource)std::numeric_limits<TResult>::max();
            }
            else
            {
                // growing unsigned -> signed conversion. this can never fail.
                inRange = true;
            }
        }
        else
        {
            // signed -> unsigned conv. check to make sure the source value isn't negative.
            inRange = src >= 0;
        }
    }

    if (inRange)
    {
        // conv_ovf with floating point inputs is handled in ConvOvfFpHelper, so all possible conv_ovfs in this function are
        // from int64 into int32-or-smaller, or int32-or-smaller into int16-or-smaller.
        // The spec says that conversion results are stored on the evaluation stack as signed, so we always want to convert
        //  the final truncated result into int32_t before storing it on the stack, even if the truncated result is unsigned.
        TResult result = (TResult)src;
        if (sizeof (TResult) < 4)
            LOCAL_VAR(ip[1], int32_t) = (int32_t)result;
        else
            LOCAL_VAR(ip[1], TResult) = result;
    }
    else
    {
        COMPlusThrow(kOverflowException);
    }
}

static float CkfiniteHelper(float value)
{
    if (!std::isfinite(value))
        COMPlusThrow(kArithmeticException);
    return value;
}

static double CkfiniteHelper(double value)
{
    if (!std::isfinite(value))
        COMPlusThrow(kArithmeticException);
    return value;
}

void* DoGenericLookup(void* genericVarAsPtr, InterpGenericLookup* pLookup)
{
    // TODO! If this becomes a performance bottleneck, we could expand out the various permutations of this
    // so that we have 24 versions of lookup (or 48 is we allow for avoiding the null check), do the only
    // if check to figure out which one to use, and then have the rest of the logic be straight-line code.
    MethodTable *pMT = NULL;
    MethodDesc* pMD = NULL;
    uint8_t* lookup;
    if (pLookup->lookupType == InterpGenericLookupType::This)
    {
        OBJECTREF thisPtr = ObjectToOBJECTREF((Object*)genericVarAsPtr);
        NULL_CHECK(thisPtr);
        pMT = thisPtr->GetMethodTable();
        lookup = (uint8_t*)pMT;
    }
    else if (pLookup->lookupType == InterpGenericLookupType::Class)
    {
        pMT = (MethodTable*)genericVarAsPtr;
        lookup = (uint8_t*)pMT;
    }
    else
    {
        _ASSERTE(pLookup->lookupType == InterpGenericLookupType::Method);
        pMD = (MethodDesc*)genericVarAsPtr;
        lookup = (uint8_t*)pMD;
    }
    void* result = 0;

    uint16_t indirections = pLookup->indirections;
    if (indirections == 0)
    {
        return lookup;
    }
    else if (indirections != InterpGenericLookup_UseHelper)
    {
        lookup = VolatileLoadWithoutBarrier((uint8_t**)(lookup + pLookup->offsets[0]));
        if (indirections >= 3)
            lookup = VolatileLoadWithoutBarrier((uint8_t**)(lookup + pLookup->offsets[1]));
        if (indirections >= 4)
            lookup = VolatileLoadWithoutBarrier((uint8_t**)(lookup + pLookup->offsets[2]));
        do {
            size_t lastOffset = pLookup->offsets[indirections - 1];
            if (pLookup->sizeOffset != CORINFO_NO_SIZE_CHECK)
            {
                /* Last indirection to check is the size*/
                size_t size = VolatileLoadWithoutBarrier((size_t*)(lookup + pLookup->sizeOffset));
                if (size <= lastOffset)
                {
                    result = GenericHandleCommon(pMD, pMT, pLookup->signature);
                    break;
                }
            }
            lookup = VolatileLoadWithoutBarrier((uint8_t**)(lookup + lastOffset));

            if (lookup == NULL)
            {
                result = GenericHandleCommon(pMD, pMT, pLookup->signature);
                break;
            }
            result = lookup;
        } while (0);
    }
    else
    {
        return GenericHandleCommon(pMD, pMT, pLookup->signature);
    }
    return result;
}

void AsyncHelpers_ResumeInterpreterContinuation(QCall::ObjectHandleOnStack cont, uint8_t* resultStorage)
{
    QCALL_CONTRACT;

    BEGIN_QCALL;

    GCX_COOP();

    Thread *pThread = GetThread();
    InterpThreadContext *threadContext = pThread->GetInterpThreadContext();
    if (threadContext == nullptr || threadContext->pStackStart == nullptr)
    {
        COMPlusThrow(kOutOfMemoryException);
    }
    int8_t *sp = threadContext->pStackPointer;

    // This construct ensures that the InterpreterFrame is always stored at a higher address than the
    // InterpMethodContextFrame. This is important for the stack walking code.
    struct Frames
    {
        InterpMethodContextFrame interpMethodContextFrame = {0};
        InterpreterFrame interpreterFrame;

        Frames(TransitionBlock* pTransitionBlock)
        : interpreterFrame(pTransitionBlock, &interpMethodContextFrame)
        {
        }
    }
    frames(NULL);

    CONTINUATIONREF contRef = (CONTINUATIONREF)ObjectToOBJECTREF(cont.Get());
    NULL_CHECK(contRef);

    // We are working with an interpreter async continuation, move things around to get the InterpAsyncSuspendData
    size_t resumeDataOffset = offsetof(InterpAsyncSuspendData, resumeInfo);
    InterpAsyncSuspendData* pSuspendData = (InterpAsyncSuspendData*)(void*)(((uint8_t*)contRef->GetResumeInfo()) - resumeDataOffset);
    _ASSERTE(&pSuspendData->resumeInfo.Resume == contRef->GetResumeInfo());

    // All the incoming args and locals should be zeroed out, except for the continuation argument
    InterpMethod *pMethod = pSuspendData->methodStartIP->Method;
    memset(sp, 0, pMethod->allocaSize);
    *(CONTINUATIONREF*)(sp + pSuspendData->continuationArgOffset) = contRef;

    frames.interpMethodContextFrame.startIp = pSuspendData->methodStartIP;
    frames.interpMethodContextFrame.pStack = sp;

    // Figure out the exact sizes to work with, since we may be copying into a Task<T>
    // and not only into another interpreter continuation
    int32_t returnValueSize = pSuspendData->asyncMethodReturnTypePrimitiveSize;
    if (pSuspendData->asyncMethodReturnType != NULL)
    {
        if (pSuspendData->asyncMethodReturnType->IsValueType())
            returnValueSize = pSuspendData->asyncMethodReturnType->GetNumInstanceFieldBytes();
        else
            returnValueSize = sizeof(OBJECTREF);
    }
    
    void* returnValueLocation = alloca(returnValueSize < (int32_t)sizeof(StackVal) ? (int32_t)sizeof(StackVal) : returnValueSize);
    memset(returnValueLocation, 0, returnValueSize);

    frames.interpMethodContextFrame.pRetVal = (int8_t*)returnValueLocation;

    InterpExecMethod(&frames.interpreterFrame, &frames.interpMethodContextFrame, threadContext);

    if (frames.interpreterFrame.GetContinuation() == NULL)
    {
        // We had a normal return, so copy out the return value
        if (returnValueSize > 0)
        {
            if (pSuspendData->asyncMethodReturnType != NULL && pSuspendData->asyncMethodReturnType->ContainsGCPointers())
            {
                // GC refs need to be written with write barriers
                memmoveGCRefs(resultStorage, returnValueLocation, returnValueSize);
            }
            else
            {
                memcpyNoGCRefs(resultStorage, returnValueLocation, returnValueSize);
            }
        }
    }

    cont.Set(frames.interpreterFrame.GetContinuation());
    frames.interpreterFrame.Pop();

    END_QCALL;
}

static void DECLSPEC_NORETURN HandleInterpreterStackOverflow(InterpreterFrame* pInterpreterFrame)
{
    CONTRACTL
    {
        NOTHROW;
        GC_NOTRIGGER;
        MODE_COOPERATIVE;
    }
    CONTRACTL_END

    CONTEXT ctx;
    memset(&ctx, 0, sizeof(CONTEXT));
    pInterpreterFrame->SetIsFaulting(true);
    pInterpreterFrame->SetContextToInterpMethodContextFrame(&ctx);

    EXCEPTION_RECORD exceptionRecord;
    memset(&exceptionRecord, 0, sizeof(EXCEPTION_RECORD));
    exceptionRecord.ExceptionCode = STATUS_STACK_OVERFLOW;
    exceptionRecord.ExceptionAddress = (PVOID)GetIP(&ctx);

    EXCEPTION_POINTERS exceptionInfo = { &exceptionRecord, &ctx };
    EEPolicy::HandleFatalStackOverflow(&exceptionInfo);
}


void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs)
{
    CONTRACTL
    {
        GC_TRIGGERS;
        MODE_COOPERATIVE;
    }
    CONTRACTL_END;

#if defined(HOST_AMD64) && defined(HOST_WINDOWS)
    pInterpreterFrame->SetInterpExecMethodSSP((TADDR)_rdsspq());
#endif // HOST_AMD64 && HOST_WINDOWS

    const int32_t *ip;
    int8_t *stack;

    InterpMethod *pMethod = pFrame->startIp->Method;
    _ASSERTE(pMethod->CheckIntegrity());

    if (pExceptionClauseArgs == NULL)
    {
        // Start executing at the beginning of the method
        ip = pFrame->startIp->GetByteCodes();
    }
    else
    {
        // Start executing at the beginning of the exception clause
        ip = pExceptionClauseArgs->ip;
    }

    if (pFrame->pStack + pMethod->allocaSize > pThreadContext->pStackEnd)
    {
        pFrame->ip = ip;
        HandleInterpreterStackOverflow(pInterpreterFrame);
        UNREACHABLE();
    }

    pThreadContext->pStackPointer = pFrame->pStack + pMethod->allocaSize;
    stack = pFrame->pStack;

    if ((pExceptionClauseArgs != NULL) && pExceptionClauseArgs->isFilter)
    {
        // * Filter funclets are executed in the current frame, because they are executed
        //   in the first pass of EH when the frames between the current frame and the
        //   parent frame are still alive. All accesses to the locals and arguments
        //   in this case use the pExceptionClauseArgs->pFrame->pStack as a frame pointer.
        // * Catch and finally funclets are running in the parent frame directly

        // Since filters run in their own frame, we need to clear the global variables
        // so that GC doesn't pick garbage in variables that were not yet written to.
        memset(pFrame->pStack, 0, pMethod->allocaSize);
    }

    int32_t returnOffset, callArgsOffset, methodSlot;
    bool isTailcall = false;
    MethodDesc* targetMethod;

    SAVE_THE_LOWEST_SP;

MAIN_LOOP:
    try
    {
        INSTALL_MANAGED_EXCEPTION_DISPATCHER;
        INSTALL_UNWIND_AND_CONTINUE_HANDLER;
        while (true)
        {
            // Interpreter-TODO: This is only needed to enable SOS see the exact location in the interpreted method.
            // Neither the GC nor the managed debugger needs that as they walk the stack when the runtime is suspended
            // and we can save the IP to the frame at the suspension time.
            // It will be useful for testing e.g. the debug info at various locations in the current method, so let's
            // keep it for such purposes until we don't need it anymore.
            pFrame->ip = (int32_t*)ip;

            switch (*ip)
            {
#ifdef DEBUG
                case INTOP_BREAKPOINT:
                    InterpBreakpoint();
                    ip++;
                    break;
#endif
                case INTOP_INITLOCALS:
                    memset(LOCAL_VAR_ADDR(ip[1], void), 0, ip[2]);
                    ip += 3;
                    break;
                case INTOP_MEMBAR:
                    MemoryBarrier();
                    ip++;
                    break;
#ifndef FEATURE_PORTABLE_ENTRYPOINTS
                case INTOP_STORESTUBCONTEXT:
                {
                    UMEntryThunkData* thunkData = GetMostRecentUMEntryThunkData();
                    _ASSERTE(thunkData);
                    LOCAL_VAR(ip[1], void*) = thunkData;
                    ip += 2;
                    break;
                }
#endif // !FEATURE_PORTABLE_ENTRYPOINTS
                case INTOP_LDC_I4:
                    LOCAL_VAR(ip[1], int32_t) = ip[2];
                    ip += 3;
                    break;
                case INTOP_LDC_I4_0:
                    LOCAL_VAR(ip[1], int32_t) = 0;
                    ip += 2;
                    break;
                case INTOP_LDC_I8_0:
                    LOCAL_VAR(ip[1], int64_t) = 0;
                    ip += 2;
                    break;
                case INTOP_LDC_I8:
                    LOCAL_VAR(ip[1], int64_t) = (int64_t)(uint32_t)ip[2] + ((int64_t)ip[3] << 32);
                    ip += 4;
                    break;
                case INTOP_LDC_R4:
                    LOCAL_VAR(ip[1], int32_t) = ip[2];
                    ip += 3;
                    break;
                case INTOP_LDC_R8:
                    LOCAL_VAR(ip[1], int64_t) = (int64_t)(uint32_t)ip[2] + ((int64_t)ip[3] << 32);
                    ip += 4;
                    break;
                case INTOP_LDPTR:
                    LOCAL_VAR(ip[1], void*) = pMethod->pDataItems[ip[2]];
                    ip += 3;
                    break;
                case INTOP_LDPTR_DEREF:
                    LOCAL_VAR(ip[1], void*) = *(void**)pMethod->pDataItems[ip[2]];
                    ip += 3;
                    break;
                case INTOP_NULLCHECK:
                    NULL_CHECK(LOCAL_VAR(ip[1], void*));
                    ip += 2;
                    break;
                case INTOP_RET:
                    // Return stack slot sized value
                    *(int64_t*)pFrame->pRetVal = LOCAL_VAR(ip[1], int64_t);
                    goto EXIT_FRAME;
                case INTOP_RET_I1:
                    // Return int8 value
                    *(int64_t*)pFrame->pRetVal = (int8_t)LOCAL_VAR(ip[1], int32_t);
                    goto EXIT_FRAME;
                case INTOP_RET_U1:
                    // Return uint8 value
                    *(int64_t*)pFrame->pRetVal = (uint8_t)LOCAL_VAR(ip[1], int32_t);
                    goto EXIT_FRAME;
                case INTOP_RET_I2:
                    // Return int16 value
                    *(int64_t*)pFrame->pRetVal = (int16_t)LOCAL_VAR(ip[1], int32_t);
                    goto EXIT_FRAME;
                case INTOP_RET_U2:
                    // Return uint16 value
                    *(int64_t*)pFrame->pRetVal = (uint16_t)LOCAL_VAR(ip[1], int32_t);
                    goto EXIT_FRAME;
                case INTOP_RET_VT:
                    memmove(pFrame->pRetVal, LOCAL_VAR_ADDR(ip[1], void), ip[2]);
                    goto EXIT_FRAME;
                case INTOP_RET_VOID:
                    goto EXIT_FRAME;

                case INTOP_LDLOCA:
                    LOCAL_VAR(ip[1], void*) = LOCAL_VAR_ADDR(ip[2], void);
                    ip += 3;
                    break;
                case INTOP_LOAD_FRAMEVAR:
                    _ASSERTE((pExceptionClauseArgs != NULL) && (pExceptionClauseArgs->isFilter));
                    LOCAL_VAR(ip[1], void*) = pExceptionClauseArgs->pFrame->pStack;
                    ip += 2;
                    break;

#define MOV(argtype1,argtype2) \
    LOCAL_VAR(ip [1], argtype1) = LOCAL_VAR(ip [2], argtype2); \
    ip += 3;
                // When loading from a local, we might need to sign / zero extend to 4 bytes
                // which is our minimum "register" size in interp. They are only needed when
                // the address of the local is taken and we should try to optimize them out
                // because the local can't be propagated.
                case INTOP_MOV_I4_I1: MOV(int32_t, int8_t); break;
                case INTOP_MOV_I4_U1: MOV(int32_t, uint8_t); break;
                case INTOP_MOV_I4_I2: MOV(int32_t, int16_t); break;
                case INTOP_MOV_I4_U2: MOV(int32_t, uint16_t); break;
                // Normal moves between vars
                case INTOP_MOV_4: MOV(int32_t, int32_t); break;
                case INTOP_MOV_8: MOV(int64_t, int64_t); break;
#undef MOV

                case INTOP_MOV_VT:
                    memmove(LOCAL_VAR_ADDR(ip[1], void), LOCAL_VAR_ADDR(ip[2], void), ip[3]);
                    ip += 4;
                    break;

                case INTOP_CKFINITE_R4:
                    LOCAL_VAR(ip[1], float) = CkfiniteHelper(LOCAL_VAR(ip[2], float));
                    ip += 3;
                    break;
                case INTOP_CKFINITE_R8:
                    LOCAL_VAR(ip[1], double) = CkfiniteHelper(LOCAL_VAR(ip[2], double));
                    ip += 3;
                    break;

                case INTOP_CONV_R_UN_I4:
                    LOCAL_VAR(ip[1], double) = (double)LOCAL_VAR(ip[2], uint32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_R_UN_I8:
                    LOCAL_VAR(ip[1], double) = (double)LOCAL_VAR(ip[2], uint64_t);
                    ip += 3;
                    break;
                case INTOP_CONV_I1_I4:
                    LOCAL_VAR(ip[1], int32_t) = (int8_t)LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_I1_I8:
                    LOCAL_VAR(ip[1], int32_t) = (int8_t)LOCAL_VAR(ip[2], int64_t);
                    ip += 3;
                    break;
                case INTOP_CONV_I1_R4:
                    ConvFpHelper<int8_t, int32_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_I1_R8:
                    ConvFpHelper<int8_t, int32_t, double>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_U1_I4:
                    LOCAL_VAR(ip[1], int32_t) = (uint8_t)LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_U1_I8:
                    LOCAL_VAR(ip[1], int32_t) = (uint8_t)LOCAL_VAR(ip[2], int64_t);
                    ip += 3;
                    break;
                case INTOP_CONV_U1_R4:
                    ConvFpHelper<uint8_t, uint32_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_U1_R8:
                    ConvFpHelper<uint8_t, uint32_t, double>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_I2_I4:
                    LOCAL_VAR(ip[1], int32_t) = (int16_t)LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_I2_I8:
                    LOCAL_VAR(ip[1], int32_t) = (int16_t)LOCAL_VAR(ip[2], int64_t);
                    ip += 3;
                    break;
                case INTOP_CONV_I2_R4:
                    ConvFpHelper<int16_t, int32_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_I2_R8:
                    ConvFpHelper<int16_t, int32_t, double>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_U2_I4:
                    LOCAL_VAR(ip[1], int32_t) = (uint16_t)LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_U2_I8:
                    LOCAL_VAR(ip[1], int32_t) = (uint16_t)LOCAL_VAR(ip[2], int64_t);
                    ip += 3;
                    break;
                case INTOP_CONV_U2_R4:
                    ConvFpHelper<uint16_t, uint32_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_U2_R8:
                    ConvFpHelper<uint16_t, uint32_t, double>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_I4_R4:
                    ConvFpHelper<int32_t, int32_t, float>(stack, ip);
                    ip += 3;
                    break;;
                case INTOP_CONV_I4_R8:
                    ConvFpHelper<int32_t, int32_t, double>(stack, ip);
                    ip += 3;
                    break;;
                case INTOP_CONV_U4_R4:
                    ConvFpHelper<uint32_t, uint32_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_U4_R8:
                    ConvFpHelper<uint32_t, uint32_t, double>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_I8_I4:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_I8_U4:
                    LOCAL_VAR(ip[1], int64_t) = (uint32_t)LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;;
                case INTOP_CONV_I8_R4:
                    ConvFpHelper<int64_t, int64_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_I8_R8:
                    ConvFpHelper<int64_t, int64_t, double>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_R4_I4:
                    LOCAL_VAR(ip[1], float) = (float)LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_R4_I8:
                    LOCAL_VAR(ip[1], float) = (float)LOCAL_VAR(ip[2], int64_t);
                    ip += 3;
                    break;
                case INTOP_CONV_R4_R8:
                    LOCAL_VAR(ip[1], float) = (float)LOCAL_VAR(ip[2], double);
                    ip += 3;
                    break;
                case INTOP_CONV_R8_I4:
                    LOCAL_VAR(ip[1], double) = (double)LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_R8_I8:
                    LOCAL_VAR(ip[1], double) = (double)LOCAL_VAR(ip[2], int64_t);
                    ip += 3;
                    break;
                case INTOP_CONV_R8_R4:
                    LOCAL_VAR(ip[1], double) = (double)LOCAL_VAR(ip[2], float);
                    ip += 3;
                    break;
                case INTOP_CONV_U8_U4:
                    LOCAL_VAR(ip[1], uint64_t) = LOCAL_VAR(ip[2], uint32_t);
                    ip += 3;
                    break;
                case INTOP_CONV_U8_R4:
                    ConvFpHelper<uint64_t, uint64_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_U8_R8:
                    ConvFpHelper<uint64_t, uint64_t, double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_I1_I4:
                    ConvOvfHelper<int8_t, int32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I1_I8:
                    ConvOvfHelper<int8_t, int64_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I1_R4:
                    ConvOvfFpHelper<int8_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I1_R8:
                    ConvOvfFpHelper<int8_t, double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_U1_I4:
                    ConvOvfHelper<uint8_t, int32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U1_I8:
                    ConvOvfHelper<uint8_t, int64_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U1_R4:
                    ConvOvfFpHelper<uint8_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U1_R8:
                    ConvOvfFpHelper<uint8_t, double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_I2_I4:
                    ConvOvfHelper<int16_t, int32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I2_I8:
                    ConvOvfHelper<int16_t, int64_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I2_R4:
                    ConvOvfFpHelper<int16_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I2_R8:
                    ConvOvfFpHelper<int16_t, double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_U2_I4:
                    ConvOvfHelper<uint16_t, int32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U2_I8:
                    ConvOvfHelper<uint16_t, int64_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U2_R4:
                    ConvOvfFpHelper<uint16_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U2_R8:
                    ConvOvfFpHelper<uint16_t, double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_I4_U4:
                    ConvOvfHelper<int32_t, uint32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I4_I8:
                    ConvOvfHelper<int32_t, int64_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I4_R4:
                    ConvOvfFpHelper<int32_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I4_R8:
                    ConvOvfFpHelper<int32_t, double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_U4_I4:
                    ConvOvfHelper<uint32_t, int32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U4_I8:
                    ConvOvfHelper<uint32_t, int64_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U4_R4:
                    ConvOvfFpHelper<uint32_t, float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U4_R8:
                    ConvOvfFpHelper<uint32_t, double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_I8_U8:
                    ConvOvfHelper<int64_t, uint64_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I8_R4:
                    ConvOvfFpHelperI64<float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I8_R8:
                    ConvOvfFpHelperI64<double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_U8_I4:
                    ConvOvfHelper<uint64_t, int32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U8_I8:
                    ConvOvfHelper<uint64_t, int64_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U8_R4:
                    ConvOvfFpHelperU64<float>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U8_R8:
                    ConvOvfFpHelperU64<double>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_I1_U4:
                    ConvOvfHelper<int8_t, uint32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I1_U8:
                    ConvOvfHelper<int8_t, uint64_t>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_U1_U4:
                    ConvOvfHelper<uint8_t, uint32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U1_U8:
                    ConvOvfHelper<uint8_t, uint64_t>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_I2_U4:
                    ConvOvfHelper<int16_t, uint32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_I2_U8:
                    ConvOvfHelper<int16_t, uint64_t>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_U2_U4:
                    ConvOvfHelper<uint16_t, uint32_t>(stack, ip);
                    ip += 3;
                    break;
                case INTOP_CONV_OVF_U2_U8:
                    ConvOvfHelper<uint16_t, uint64_t>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_I4_U8:
                    ConvOvfHelper<int32_t, uint64_t>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_CONV_OVF_U4_U8:
                    ConvOvfHelper<uint32_t, uint64_t>(stack, ip);
                    ip += 3;
                    break;

                case INTOP_SWITCH:
                {
                    uint32_t val = LOCAL_VAR(ip[1], uint32_t);
                    uint32_t n = ip[2];
                    ip += 3;
                    if (val < n)
                    {
                        ip += val;
                        ip += *ip;
                    }
                    else
                    {
                        ip += n;
                    }
                    break;
                }

                case INTOP_SAFEPOINT:
                    if (g_TrapReturningThreads)
                    {
                        Thread *pThread = GetThread();
                        if (pThread->IsAbortRequested())
                        {
                            CallWithSEHWrapper(
                            [&pThread]() {
                                pThread->HandleThreadAbort();
                                return 0;
                            });
                        }
                        // Transition into preemptive mode to allow the GC to suspend us
                        GCX_PREEMP();
                    }
                    ip++;
                    break;

                case INTOP_BR:
                    ip += ip[1];
                    break;

#define BR_UNOP(datatype, op)           \
    if (LOCAL_VAR(ip[1], datatype) op)  \
        ip += ip[2];                    \
    else \
        ip += 3;

                case INTOP_BRFALSE_I4:
                    BR_UNOP(int32_t, == 0);
                    break;
                case INTOP_BRFALSE_I8:
                    BR_UNOP(int64_t, == 0);
                    break;
                case INTOP_BRTRUE_I4:
                    BR_UNOP(int32_t, != 0);
                    break;
                case INTOP_BRTRUE_I8:
                    BR_UNOP(int64_t, != 0);
                    break;
#undef BR_UNOP

#define BR_BINOP_COND(cond) \
    if (cond)               \
        ip += ip[3];        \
    else                    \
        ip += 4;

#define BR_BINOP(datatype, op) \
    BR_BINOP_COND(LOCAL_VAR(ip[1], datatype) op LOCAL_VAR(ip[2], datatype))

                case INTOP_BEQ_I4:
                    BR_BINOP(int32_t, ==);
                    break;
                case INTOP_BEQ_I8:
                    BR_BINOP(int64_t, ==);
                    break;
                case INTOP_BEQ_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(!isunordered(f1, f2) && f1 == f2);
                    break;
                }
                case INTOP_BEQ_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(!isunordered(d1, d2) && d1 == d2);
                    break;
                }
                case INTOP_BGE_I4:
                    BR_BINOP(int32_t, >=);
                    break;
                case INTOP_BGE_I8:
                    BR_BINOP(int64_t, >=);
                    break;
                case INTOP_BGE_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(!isunordered(f1, f2) && f1 >= f2);
                    break;
                }
                case INTOP_BGE_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(!isunordered(d1, d2) && d1 >= d2);
                    break;
                }
                case INTOP_BGT_I4:
                    BR_BINOP(int32_t, >);
                    break;
                case INTOP_BGT_I8:
                    BR_BINOP(int64_t, >);
                    break;
                case INTOP_BGT_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(!isunordered(f1, f2) && f1 > f2);
                    break;
                }
                case INTOP_BGT_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(!isunordered(d1, d2) && d1 > d2);
                    break;
                }
                case INTOP_BLT_I4:
                    BR_BINOP(int32_t, <);
                    break;
                case INTOP_BLT_I8:
                    BR_BINOP(int64_t, <);
                    break;
                case INTOP_BLT_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(!isunordered(f1, f2) && f1 < f2);
                    break;
                }
                case INTOP_BLT_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(!isunordered(d1, d2) && d1 < d2);
                    break;
                }
                case INTOP_BLE_I4:
                    BR_BINOP(int32_t, <=);
                    break;
                case INTOP_BLE_I8:
                    BR_BINOP(int64_t, <=);
                    break;
                case INTOP_BLE_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(!isunordered(f1, f2) && f1 <= f2);
                    break;
                }
                case INTOP_BLE_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(!isunordered(d1, d2) && d1 <= d2);
                    break;
                }
                case INTOP_BNE_UN_I4:
                    BR_BINOP(uint32_t, !=);
                    break;
                case INTOP_BNE_UN_I8:
                    BR_BINOP(uint64_t, !=);
                    break;
                case INTOP_BNE_UN_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(isunordered(f1, f2) || f1 != f2);
                    break;
                }
                case INTOP_BNE_UN_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(isunordered(d1, d2) || d1 != d2);
                    break;
                }
                case INTOP_BGE_UN_I4:
                    BR_BINOP(uint32_t, >=);
                    break;
                case INTOP_BGE_UN_I8:
                    BR_BINOP(uint64_t, >=);
                    break;
                case INTOP_BGE_UN_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(isunordered(f1, f2) || f1 >= f2);
                    break;
                }
                case INTOP_BGE_UN_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(isunordered(d1, d2) || d1 >= d2);
                    break;
                }
                case INTOP_BGT_UN_I4:
                    BR_BINOP(uint32_t, >);
                    break;
                case INTOP_BGT_UN_I8:
                    BR_BINOP(uint64_t, >);
                    break;
                case INTOP_BGT_UN_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(isunordered(f1, f2) || f1 > f2);
                    break;
                }
                case INTOP_BGT_UN_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(isunordered(d1, d2) || d1 > d2);
                    break;
                }
                case INTOP_BLE_UN_I4:
                    BR_BINOP(uint32_t, <=);
                    break;
                case INTOP_BLE_UN_I8:
                    BR_BINOP(uint64_t, <=);
                    break;
                case INTOP_BLE_UN_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(isunordered(f1, f2) || f1 <= f2);
                    break;
                }
                case INTOP_BLE_UN_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(isunordered(d1, d2) || d1 <= d2);
                    break;
                }
                case INTOP_BLT_UN_I4:
                    BR_BINOP(uint32_t, <);
                    break;
                case INTOP_BLT_UN_I8:
                    BR_BINOP(uint64_t, <);
                    break;
                case INTOP_BLT_UN_R4:
                {
                    float f1 = LOCAL_VAR(ip[1], float);
                    float f2 = LOCAL_VAR(ip[2], float);
                    BR_BINOP_COND(isunordered(f1, f2) || f1 < f2);
                    break;
                }
                case INTOP_BLT_UN_R8:
                {
                    double d1 = LOCAL_VAR(ip[1], double);
                    double d2 = LOCAL_VAR(ip[2], double);
                    BR_BINOP_COND(isunordered(d1, d2) || d1 < d2);
                    break;
                }
#undef BR_BINOP_COND
#undef BR_BINOP

                case INTOP_ADD_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) + LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_ADD_I8:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) + LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;
                case INTOP_ADD_R4:
                    LOCAL_VAR(ip[1], float) = LOCAL_VAR(ip[2], float) + LOCAL_VAR(ip[3], float);
                    ip += 4;
                    break;
                case INTOP_ADD_R8:
                    LOCAL_VAR(ip[1], double) = LOCAL_VAR(ip[2], double) + LOCAL_VAR(ip[3], double);
                    ip += 4;
                    break;
                case INTOP_ADD_I4_IMM:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) + ip[3];
                    ip += 4;
                    break;
                case INTOP_ADD_I8_IMM:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) + ip[3];
                    ip += 4;
                    break;
                case INTOP_ADD_OVF_I4:
                {
                    int32_t i1 = LOCAL_VAR(ip[2], int32_t);
                    int32_t i2 = LOCAL_VAR(ip[3], int32_t);
                    int32_t i3;
                    if (!ClrSafeInt<int32_t>::addition(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int32_t) = i3;
                    ip += 4;
                    break;
                }
                case INTOP_ADD_OVF_I8:
                {
                    int64_t i1 = LOCAL_VAR(ip[2], int64_t);
                    int64_t i2 = LOCAL_VAR(ip[3], int64_t);
                    int64_t i3;
                    if (!ClrSafeInt<int64_t>::addition(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int64_t) = i3;
                    ip += 4;
                    break;
                }
                case INTOP_ADD_OVF_UN_I4:
                {
                    uint32_t i1 = LOCAL_VAR(ip[2], uint32_t);
                    uint32_t i2 = LOCAL_VAR(ip[3], uint32_t);
                    uint32_t i3;
                    if (!ClrSafeInt<uint32_t>::addition(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], uint32_t) = i3;
                    ip += 4;
                    break;
                }
                case INTOP_ADD_OVF_UN_I8:
                {
                    uint64_t i1 = LOCAL_VAR(ip[2], uint64_t);
                    uint64_t i2 = LOCAL_VAR(ip[3], uint64_t);
                    uint64_t i3;
                    if (!ClrSafeInt<uint64_t>::addition(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], uint64_t) = i3;
                    ip += 4;
                    break;
                }
                case INTOP_SUB_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) - LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_SUB_I8:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) - LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;
                case INTOP_SUB_R4:
                    LOCAL_VAR(ip[1], float) = LOCAL_VAR(ip[2], float) - LOCAL_VAR(ip[3], float);
                    ip += 4;
                    break;
                case INTOP_SUB_R8:
                    LOCAL_VAR(ip[1], double) = LOCAL_VAR(ip[2], double) - LOCAL_VAR(ip[3], double);
                    ip += 4;
                    break;

                case INTOP_SUB_OVF_I4:
                {
                    int32_t i1 = LOCAL_VAR(ip[2], int32_t);
                    int32_t i2 = LOCAL_VAR(ip[3], int32_t);
                    int32_t i3;
                    if (!ClrSafeInt<int32_t>::subtraction(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int32_t) = i3;
                    ip += 4;
                    break;
                }
                case INTOP_SUB_OVF_I8:
                {
                    int64_t i1 = LOCAL_VAR(ip[2], int64_t);
                    int64_t i2 = LOCAL_VAR(ip[3], int64_t);
                    int64_t i3;
                    if (!ClrSafeInt<int64_t>::subtraction(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int64_t) = i3;
                    ip += 4;
                    break;
                }
                case INTOP_SUB_OVF_UN_I4:
                {
                    uint32_t i1 = LOCAL_VAR(ip[2], uint32_t);
                    uint32_t i2 = LOCAL_VAR(ip[3], uint32_t);
                    uint32_t i3;
                    if (!ClrSafeInt<uint32_t>::subtraction(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], uint32_t) = i3;
                    ip += 4;
                    break;
                }
                case INTOP_SUB_OVF_UN_I8:
                {
                    uint64_t i1 = LOCAL_VAR(ip[2], uint64_t);
                    uint64_t i2 = LOCAL_VAR(ip[3], uint64_t);
                    uint64_t i3;
                    if (!ClrSafeInt<uint64_t>::subtraction(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], uint64_t) = i3;
                    ip += 4;
                    break;
                }

                case INTOP_MUL_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) * LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_MUL_I8:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) * LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;
                case INTOP_MUL_R4:
                    LOCAL_VAR(ip[1], float) = LOCAL_VAR(ip[2], float) * LOCAL_VAR(ip[3], float);
                    ip += 4;
                    break;
                case INTOP_MUL_R8:
                    LOCAL_VAR(ip[1], double) = LOCAL_VAR(ip[2], double) * LOCAL_VAR(ip[3], double);
                    ip += 4;
                    break;
                case INTOP_MUL_OVF_I4:
                {
                    int32_t i1 = LOCAL_VAR(ip[2], int32_t);
                    int32_t i2 = LOCAL_VAR(ip[3], int32_t);
                    int32_t i3;
                    if (!ClrSafeInt<int32_t>::multiply(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int32_t) = i3;
                    ip += 4;
                    break;
                }

                case INTOP_MUL_OVF_I8:
                {
                    int64_t i1 = LOCAL_VAR(ip[2], int64_t);
                    int64_t i2 = LOCAL_VAR(ip[3], int64_t);
                    int64_t i3;
                    if (!ClrSafeInt<int64_t>::multiply(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int64_t) = i3;
                    ip += 4;
                    break;
                }

                case INTOP_MUL_OVF_UN_I4:
                {
                    uint32_t i1 = LOCAL_VAR(ip[2], uint32_t);
                    uint32_t i2 = LOCAL_VAR(ip[3], uint32_t);
                    uint32_t i3;
                    if (!ClrSafeInt<uint32_t>::multiply(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], uint32_t) = i3;
                    ip += 4;
                    break;
                }

                case INTOP_MUL_OVF_UN_I8:
                {
                    uint64_t i1 = LOCAL_VAR(ip[2], uint64_t);
                    uint64_t i2 = LOCAL_VAR(ip[3], uint64_t);
                    uint64_t i3;
                    if (!ClrSafeInt<uint64_t>::multiply(i1, i2, i3))
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], uint64_t) = i3;
                    ip += 4;
                    break;
                }
                case INTOP_DIV_I4:
                {
                    int32_t i1 = LOCAL_VAR(ip[2], int32_t);
                    int32_t i2 = LOCAL_VAR(ip[3], int32_t);
                    if (i2 == 0)
                        COMPlusThrow(kDivideByZeroException);
                    if (i2 == -1 && i1 == INT32_MIN)
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int32_t) = i1 / i2;
                    ip += 4;
                    break;
                }
                case INTOP_DIV_I8:
                {
                    int64_t l1 = LOCAL_VAR(ip[2], int64_t);
                    int64_t l2 = LOCAL_VAR(ip[3], int64_t);
                    if (l2 == 0)
                        COMPlusThrow(kDivideByZeroException);
                    if (l2 == -1 && l1 == INT64_MIN)
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int64_t) = l1 / l2;
                    ip += 4;
                    break;
                }
                case INTOP_DIV_R4:
                    LOCAL_VAR(ip[1], float) = LOCAL_VAR(ip[2], float) / LOCAL_VAR(ip[3], float);
                    ip += 4;
                    break;
                case INTOP_DIV_R8:
                    LOCAL_VAR(ip[1], double) = LOCAL_VAR(ip[2], double) / LOCAL_VAR(ip[3], double);
                    ip += 4;
                    break;
                case INTOP_DIV_UN_I4:
                {
                    uint32_t i2 = LOCAL_VAR(ip[3], uint32_t);
                    if (i2 == 0)
                        COMPlusThrow(kDivideByZeroException);
                    LOCAL_VAR(ip[1], uint32_t) = LOCAL_VAR(ip[2], uint32_t) / i2;
                    ip += 4;
                    break;
                }
                case INTOP_DIV_UN_I8:
                {
                    uint64_t l2 = LOCAL_VAR(ip[3], uint64_t);
                    if (l2 == 0)
                        COMPlusThrow(kDivideByZeroException);
                    LOCAL_VAR(ip[1], uint64_t) = LOCAL_VAR(ip[2], uint64_t) / l2;
                    ip += 4;
                    break;
                }

                case INTOP_REM_I4:
                {
                    int32_t i1 = LOCAL_VAR(ip[2], int32_t);
                    int32_t i2 = LOCAL_VAR(ip[3], int32_t);
                    if (i2 == 0)
                        COMPlusThrow(kDivideByZeroException);
                    if (i2 == -1 && i1 == INT32_MIN)
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int32_t) = i1 % i2;
                    ip += 4;
                    break;
                }
                case INTOP_REM_I8:
                {
                    int64_t l1 = LOCAL_VAR(ip[2], int64_t);
                    int64_t l2 = LOCAL_VAR(ip[3], int64_t);
                    if (l2 == 0)
                        COMPlusThrow(kDivideByZeroException);
                    if (l2 == -1 && l1 == INT64_MIN)
                        COMPlusThrow(kOverflowException);
                    LOCAL_VAR(ip[1], int64_t) = l1 % l2;
                    ip += 4;
                    break;
                }
                case INTOP_REM_R4:
                    LOCAL_VAR(ip[1], float) = fmodf(LOCAL_VAR(ip[2], float), LOCAL_VAR(ip[3], float));
                    ip += 4;
                    break;
                case INTOP_REM_R8:
                    LOCAL_VAR(ip[1], double) = fmod(LOCAL_VAR(ip[2], double), LOCAL_VAR(ip[3], double));
                    ip += 4;
                    break;
                case INTOP_REM_UN_I4:
                {
                    uint32_t i2 = LOCAL_VAR(ip[3], uint32_t);
                    if (i2 == 0)
                        COMPlusThrow(kDivideByZeroException);
                    LOCAL_VAR(ip[1], uint32_t) = LOCAL_VAR(ip[2], uint32_t) % i2;
                    ip += 4;
                    break;
                }
                case INTOP_REM_UN_I8:
                {
                    uint64_t l2 = LOCAL_VAR(ip[3], uint64_t);
                    if (l2 == 0)
                        COMPlusThrow(kDivideByZeroException);
                    LOCAL_VAR(ip[1], uint64_t) = LOCAL_VAR(ip[2], uint64_t) % l2;
                    ip += 4;
                    break;
                }

                case INTOP_SHL_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) << LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_SHL_I8:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) << LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_SHR_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) >> LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_SHR_I8:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) >> LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_SHR_UN_I4:
                    LOCAL_VAR(ip[1], uint32_t) = LOCAL_VAR(ip[2], uint32_t) >> LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_SHR_UN_I8:
                    LOCAL_VAR(ip[1], uint64_t) = LOCAL_VAR(ip[2], uint64_t) >> LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;

                case INTOP_NEG_I4:
                    LOCAL_VAR(ip[1], int32_t) = - LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_NEG_I8:
                    LOCAL_VAR(ip[1], int64_t) = - LOCAL_VAR(ip[2], int64_t);
                    ip += 3;
                    break;
                case INTOP_NEG_R4:
                    LOCAL_VAR(ip[1], float) = - LOCAL_VAR(ip[2], float);
                    ip += 3;
                    break;
                case INTOP_NEG_R8:
                    LOCAL_VAR(ip[1], double) = - LOCAL_VAR(ip[2], double);
                    ip += 3;
                    break;
                case INTOP_NOT_I4:
                    LOCAL_VAR(ip[1], int32_t) = ~ LOCAL_VAR(ip[2], int32_t);
                    ip += 3;
                    break;
                case INTOP_NOT_I8:
                    LOCAL_VAR(ip[1], int64_t) = ~ LOCAL_VAR(ip[2], int64_t);
                    ip += 3;
                    break;

                case INTOP_AND_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) & LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_AND_I8:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) & LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;
                case INTOP_OR_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) | LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_OR_I8:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) | LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;
                case INTOP_XOR_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) ^ LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_XOR_I8:
                    LOCAL_VAR(ip[1], int64_t) = LOCAL_VAR(ip[2], int64_t) ^ LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;

#define CMP_BINOP_FP(datatype, op, noOrderVal)      \
    do {                                            \
        datatype f1 = LOCAL_VAR(ip[2], datatype);   \
        datatype f2 = LOCAL_VAR(ip[3], datatype);   \
        if (isunordered(f1, f2))                    \
            LOCAL_VAR(ip[1], int32_t) = noOrderVal; \
        else                                        \
            LOCAL_VAR(ip[1], int32_t) = f1 op f2;   \
        ip += 4;                                    \
    } while (0)

                case INTOP_CZERO_I:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], intptr_t) != 0;
                    ip += 4;
                    break;
                case INTOP_CEQ_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) == LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_CEQ_I8:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int64_t) == LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;
                case INTOP_CEQ_R4:
                    CMP_BINOP_FP(float, ==, 0);
                    break;
                case INTOP_CEQ_R8:
                    CMP_BINOP_FP(double, ==, 0);
                    break;

                case INTOP_CGT_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) > LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_CGT_I8:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int64_t) > LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;
                case INTOP_CGT_R4:
                    CMP_BINOP_FP(float, >, 0);
                    break;
                case INTOP_CGT_R8:
                    CMP_BINOP_FP(double, >, 0);
                    break;

                case INTOP_CGT_UN_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], uint32_t) > LOCAL_VAR(ip[3], uint32_t);
                    ip += 4;
                    break;
                case INTOP_CGT_UN_I8:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], uint64_t) > LOCAL_VAR(ip[3], uint64_t);
                    ip += 4;
                    break;
                case INTOP_CGT_UN_R4:
                    CMP_BINOP_FP(float, >, 1);
                    break;
                case INTOP_CGT_UN_R8:
                    CMP_BINOP_FP(double, >, 1);
                    break;

                case INTOP_CLT_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int32_t) < LOCAL_VAR(ip[3], int32_t);
                    ip += 4;
                    break;
                case INTOP_CLT_I8:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], int64_t) < LOCAL_VAR(ip[3], int64_t);
                    ip += 4;
                    break;
                case INTOP_CLT_R4:
                    CMP_BINOP_FP(float, <, 0);
                    break;
                case INTOP_CLT_R8:
                    CMP_BINOP_FP(double, <, 0);
                    break;

                case INTOP_CLT_UN_I4:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], uint32_t) < LOCAL_VAR(ip[3], uint32_t);
                    ip += 4;
                    break;
                case INTOP_CLT_UN_I8:
                    LOCAL_VAR(ip[1], int32_t) = LOCAL_VAR(ip[2], uint64_t) < LOCAL_VAR(ip[3], uint64_t);
                    ip += 4;
                    break;
                case INTOP_CLT_UN_R4:
                    CMP_BINOP_FP(float, <, 1);
                    break;
                case INTOP_CLT_UN_R8:
                    CMP_BINOP_FP(double, <, 1);
                    break;
#undef CMP_BINOP_FP

#define LDIND(dtype, ftype)                                 \
    do {                                                    \
        char *src = LOCAL_VAR(ip[2], char*);                \
        NULL_CHECK(src);                                    \
        LOCAL_VAR(ip[1], dtype) = *(ftype*)(src + ip[3]);   \
        ip += 4;                                            \
    } while (0)

                case INTOP_LDIND_I1:
                    LDIND(int32_t, int8_t);
                    break;
                case INTOP_LDIND_U1:
                    LDIND(int32_t, uint8_t);
                    break;
                case INTOP_LDIND_I2:
                    LDIND(int32_t, int16_t);
                    break;
                case INTOP_LDIND_U2:
                    LDIND(int32_t, uint16_t);
                    break;
                case INTOP_LDIND_I4:
                    LDIND(int32_t, int32_t);
                    break;
                case INTOP_LDIND_I8:
                    LDIND(int64_t, int64_t);
                    break;
                case INTOP_LDIND_R4:
                    LDIND(float, float);
                    break;
                case INTOP_LDIND_R8:
                    LDIND(double, double);
                    break;
#undef LDIND
                case INTOP_LDIND_VT:
                {
                    char *src = LOCAL_VAR(ip[2], char*);
                    NULL_CHECK(src);
                    memcpy(LOCAL_VAR_ADDR(ip[1], void), (char*)src + ip[3], ip[4]);
                    ip += 5;
                    break;
                }

#define STIND(dtype, ftype)                                         \
    do                                                              \
    {                                                               \
        char *dst = LOCAL_VAR(ip[1], char*);                        \
        NULL_CHECK(dst);                                            \
        *(ftype*)(dst + ip[3]) = (ftype)(LOCAL_VAR(ip[2], dtype));  \
        ip += 4;                                                    \
    } while (0)

                case INTOP_STIND_I1:
                    STIND(int32_t, int8_t);
                    break;
                case INTOP_STIND_U1:
                    STIND(int32_t, uint8_t);
                    break;
                case INTOP_STIND_I2:
                    STIND(int32_t, int16_t);
                    break;
                case INTOP_STIND_U2:
                    STIND(int32_t, uint16_t);
                    break;
                case INTOP_STIND_I4:
                    STIND(int32_t, int32_t);
                    break;
                case INTOP_STIND_I8:
                    STIND(int64_t, int64_t);
                    break;
                case INTOP_STIND_R4:
                    STIND(float, float);
                    break;
                case INTOP_STIND_R8:
                    STIND(double, double);
                    break;
#undef STIND
                case INTOP_STIND_O:
                {
                    char *dst = LOCAL_VAR(ip[1], char*);
                    OBJECTREF storeObj = LOCAL_VAR(ip[2], OBJECTREF);
                    NULL_CHECK(dst);
                    SetObjectReferenceUnchecked((OBJECTREF*)(dst + ip[3]), storeObj);
                    ip += 4;
                    break;
                }
                case INTOP_STIND_VT_NOREF:
                {
                    char *dest = LOCAL_VAR(ip[1], char*);
                    NULL_CHECK(dest);
                    memcpyNoGCRefs(dest + ip[3], LOCAL_VAR_ADDR(ip[2], void), ip[4]);
                    ip += 5;
                    break;
                }
                case INTOP_STIND_VT:
                {
                    MethodTable *pMT = (MethodTable*)pMethod->pDataItems[ip[4]];
                    char *dest = LOCAL_VAR(ip[1], char*);
                    NULL_CHECK(dest);
                    CopyValueClassUnchecked(dest + ip[3], LOCAL_VAR_ADDR(ip[2], void), pMT);
                    ip += 5;
                    break;
                }
                case INTOP_LDFLDA:
                {
                    char *src = LOCAL_VAR(ip[2], char*);
                    NULL_CHECK(src);
                    LOCAL_VAR(ip[1], char*) = src + ip[3];
                    ip += 4;
                    break;
                }

                case INTOP_CALL_HELPER_P_P:
                {
                    void* helperArg = pMethod->pDataItems[ip[3]];

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_P helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_P>(pMethod, ip[2], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass argument to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg;

                        targetMethod = pILTargetMethod;
                        ip += 4;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    LOCAL_VAR(ip[1], void*) = helperFtn(helperArg);
                    ip += 4;
                    break;
                }

                case INTOP_CALL_HELPER_P_S:
                {
                    void* helperArg = LOCAL_VAR(ip[2], void*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_P helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_P>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass argument to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg;
                        targetMethod = pILTargetMethod;
                        ip += 4;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    LOCAL_VAR(ip[1], void*) = helperFtn(helperArg);
                    ip += 4;
                    break;
                }

                case INTOP_CALL_HELPER_P_PS:
                {
                    void* helperArg1 = pMethod->pDataItems[ip[4]];
                    void* helperArg2 = LOCAL_VAR(ip[2], void*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_PP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_PP>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;

                        targetMethod = pILTargetMethod;
                        ip += 5;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    LOCAL_VAR(ip[1], void*) = helperFtn(helperArg1, helperArg2);
                    ip += 5;
                    break;
                }

                case INTOP_CALL_HELPER_P_SP:
                {
                    void* helperArg1 = LOCAL_VAR(ip[2], void*);
                    void* helperArg2 = pMethod->pDataItems[ip[4]];

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_PP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_PP>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;

                        targetMethod = pILTargetMethod;
                        ip += 5;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    LOCAL_VAR(ip[1], void*) = helperFtn(helperArg1, helperArg2);
                    ip += 5;
                    break;
                }

                case INTOP_CALL_HELPER_P_G:
                {
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[4]];
                    void* helperArg = DoGenericLookup(LOCAL_VAR(ip[2], void*), pLookup);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_P helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_P>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass argument to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg;

                        targetMethod = pILTargetMethod;
                        ip += 5;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    LOCAL_VAR(ip[1], void*) = helperFtn(helperArg);
                    ip += 5;
                    break;
                }

                case INTOP_CALL_HELPER_P_GS:
                {
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[5]];
                    void* helperArg1 = DoGenericLookup(LOCAL_VAR(ip[2], void*), pLookup);
                    void* helperArg2 = LOCAL_VAR(ip[3], void*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_PP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_PP>(pMethod, ip[4], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;

                        targetMethod = pILTargetMethod;
                        ip += 6;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    LOCAL_VAR(ip[1], void*) = helperFtn(helperArg1, helperArg2);
                    ip += 6;
                    break;
                }

                case INTOP_CALL_HELPER_P_GA:
                {
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[5]];
                    void* helperArg1 = DoGenericLookup(LOCAL_VAR(ip[2], void*), pLookup);
                    void* helperArg2 = LOCAL_VAR_ADDR(ip[3], void*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_PP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_PP>(pMethod, ip[4], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;

                        targetMethod = pILTargetMethod;
                        ip += 6;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    LOCAL_VAR(ip[1], void*) = helperFtn(helperArg1, helperArg2);
                    ip += 6;
                    break;
                }

                case INTOP_CALL_HELPER_P_PA:
                {
                    void* helperArg1 = pMethod->pDataItems[ip[4]];
                    void* helperArg2 = LOCAL_VAR_ADDR(ip[2], void*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_PP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_PP>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;

                        targetMethod = pILTargetMethod;
                        ip += 5;
                        goto CALL_INTERP_METHOD;
                    }

                    LOCAL_VAR(ip[1], void*) = helperFtn(helperArg1, helperArg2);
                    ip += 5;
                    break;
                }

                case INTOP_CALL_HELPER_V_AGS:
                {
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[5]];
                    void* helperArg1 = LOCAL_VAR_ADDR(ip[1], void*);
                    void* helperArg2 = DoGenericLookup(LOCAL_VAR(ip[2], void*), pLookup);
                    void* helperArg3 = LOCAL_VAR(ip[3], void*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_V_PPP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_V_PPP>(pMethod, ip[4], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;
                        LOCAL_VAR(callArgsOffset + 2 * INTERP_STACK_SLOT_SIZE, void*) = helperArg3;

                        targetMethod = pILTargetMethod;
                        ip += 6;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    helperFtn(helperArg1, helperArg2, helperArg3);
                    ip += 6;
                    break;
                }

                case INTOP_CALL_HELPER_V_APS:
                {
                    void* helperArg1 = LOCAL_VAR_ADDR(ip[1], void*);
                    void* helperArg2 = pMethod->pDataItems[ip[4]];
                    void* helperArg3 = LOCAL_VAR(ip[2], void*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_V_PPP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_V_PPP>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;
                        LOCAL_VAR(callArgsOffset + 2 * INTERP_STACK_SLOT_SIZE, void*) = helperArg3;

                        targetMethod = pILTargetMethod;
                        ip += 5;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    helperFtn(helperArg1, helperArg2, helperArg3);
                    ip += 5;
                    break;
                }

                case INTOP_CALL_HELPER_V_SA:
                {
                    void* helperArg1 = LOCAL_VAR(ip[1], void*);
                    void* helperArg2 = LOCAL_VAR_ADDR(ip[2], void*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_V_PP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_V_PP>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;

                        targetMethod = pILTargetMethod;
                        ip += 4;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    helperFtn(helperArg1, helperArg2);
                    ip += 4;
                    break;
                }
                case INTOP_CALL_HELPER_V_SS:
                {
                    void* helperArg1 = LOCAL_VAR(ip[1], void*);
                    void* helperArg2 = LOCAL_VAR(ip[2], void*);
                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_V_PP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_V_PP>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = pMethod->allocaSize;
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;

                        targetMethod = pILTargetMethod;
                        ip += 4;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    helperFtn(helperArg1, helperArg2);
                    ip += 4;
                    break;
                }

                case INTOP_CALL_HELPER_V_SSS:
                {
                    void* helperArg1 = LOCAL_VAR(ip[1], void*);
                    void* helperArg2 = LOCAL_VAR(ip[2], void*);
                    void* helperArg3 = LOCAL_VAR(ip[3], void*);
                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_V_PPP helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_V_PPP>(pMethod, ip[4], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = pMethod->allocaSize;
                        callArgsOffset = pMethod->allocaSize;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = helperArg1;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = helperArg2;
                        LOCAL_VAR(callArgsOffset + 2 * INTERP_STACK_SLOT_SIZE, void*) = helperArg3;

                        targetMethod = pILTargetMethod;
                        ip += 5;
                        goto CALL_INTERP_METHOD;
                    }

                    _ASSERTE(helperFtn != NULL);
                    helperFtn(helperArg1, helperArg2, helperArg3);
                    ip += 5;
                    break;
                }

                case INTOP_CALLVIRT_TAIL:
                case INTOP_CALLVIRT:
                {
                    isTailcall = (*ip == INTOP_CALLVIRT_TAIL);
                    returnOffset = ip[1];
                    callArgsOffset = ip[2];
                    methodSlot = ip[3];

                    MethodDesc *pMD = (MethodDesc*)pMethod->pDataItems[methodSlot];

                    OBJECTREF *pThisArg = LOCAL_VAR_ADDR(callArgsOffset, OBJECTREF);
                    NULL_CHECK(*pThisArg);

                    // Interpreter-TODO
                    // This needs to be optimized, not operating at MethodDesc level, rather with ftnptr
                    // slots containing the interpreter IR pointer
                    targetMethod = CallWithSEHWrapper(
                        [&pMD, &pThisArg]() {
                            return pMD->GetMethodDescOfVirtualizedCode(pThisArg, pMD->GetMethodTable());
                        });

                    ip += 4;
                    goto CALL_INTERP_METHOD;
                }

                case INTOP_CALLI_TAIL:
                case INTOP_CALLI:
                {
                    isTailcall = (*ip == INTOP_CALLI_TAIL);
                    returnOffset = ip[1];
                    int8_t* returnValueAddress = LOCAL_VAR_ADDR(returnOffset, int8_t);
                    callArgsOffset = ip[2];
                    int8_t* callArgsAddress = LOCAL_VAR_ADDR(callArgsOffset, int8_t);
                    int32_t calliFunctionPointerVar = ip[3];
                    int32_t calliCookie = ip[4];
                    int32_t flags = ip[5];

                    void* cookie = pMethod->pDataItems[calliCookie];
                    ip += 6;

                    // Save current execution state for when we return from called method
                    pFrame->ip = ip;

                    PCODE calliFunctionPointer = LOCAL_VAR(calliFunctionPointerVar, PCODE);
                    _ASSERTE(calliFunctionPointer);

                    if (flags & (int32_t)CalliFlags::PInvoke)
                    {
                        if (flags & (int32_t)CalliFlags::SuppressGCTransition)
                        {
                            InvokeUnmanagedCalli(calliFunctionPointer, cookie, callArgsAddress, returnValueAddress);
                        }
                        else
                        {
                            InvokeUnmanagedCalliWithTransition(calliFunctionPointer, cookie, stack, pFrame, callArgsAddress, returnValueAddress);
                        }
                    }
#ifndef FEATURE_PORTABLE_ENTRYPOINTS
// If we're not using portable entrypoints, we can use NonVirtualEntry2MethodDesc to figure out where tailcalls go. Since this is
// somewhat expensive, we only do it for tailcalls which are relatively rare.
// TODO-Interpreter: It is plausible that we might want to do the NonVirtualEntry2MethodDesc check for non-tailcall calli as well
//                   or possibly a slight variant where we build a path for NonVitualEntry2MethodDesc which is lock-free but might fail
                    else if (isTailcall && (targetMethod = NonVirtualEntry2MethodDesc(calliFunctionPointer)) != NULL)
                    {
                        goto CALL_INTERP_METHOD;
                    }
#endif // !FEATURE_PORTABLE_ENTRYPOINTS
                    else
                    {
#ifdef FEATURE_PORTABLE_ENTRYPOINTS
                        // WASM-TODO: We may end up here with native JIT helper entrypoint without MethodDesc
                        // that CALL_INTERP_METHOD is not able to handle. This is a potential problem for
                        // interpreter<->native code stub generator.
                        // https://github.com/dotnet/runtime/pull/119516#discussion_r2337631271
                        if (!PortableEntryPoint::HasNativeEntryPoint(calliFunctionPointer))
                        {
                            targetMethod = PortableEntryPoint::GetMethodDesc(calliFunctionPointer);
                            goto CALL_INTERP_METHOD;
                        }
#endif // FEATURE_PORTABLE_ENTRYPOINTS
                        InvokeCalliStub(calliFunctionPointer, cookie, callArgsAddress, returnValueAddress, pInterpreterFrame->GetContinuationPtr());
                    }

                    break;
                }

                case INTOP_CALL_PINVOKE:
                {
                    // This opcode handles p/invokes that don't use a managed wrapper for marshaling. These
                    //  calls are special in that they need an InlinedCallFrame in order for proper EH to happen

                    isTailcall = false;
                    returnOffset = ip[1];
                    callArgsOffset = ip[2];
                    int8_t* callArgsAddress = LOCAL_VAR_ADDR(callArgsOffset, int8_t);
                    int8_t* returnValueAddress = LOCAL_VAR_ADDR(returnOffset, int8_t);
                    methodSlot = ip[3];
                    int32_t targetAddrSlot = ip[4];
                    int32_t flags = ip[5];

                    ip += 6;
                    targetMethod = (MethodDesc*)pMethod->pDataItems[methodSlot];
                    PCODE callTarget = (flags & (int32_t)PInvokeCallFlags::Indirect)
                        ? *(PCODE *)pMethod->pDataItems[targetAddrSlot]
                        : (PCODE)pMethod->pDataItems[targetAddrSlot];

                    // Save current execution state for when we return from called method
                    pFrame->ip = ip;

                    if (flags & (int32_t)PInvokeCallFlags::SuppressGCTransition)
                    {
                        InvokeUnmanagedMethod(targetMethod, callArgsAddress, returnValueAddress, callTarget);
                    }
                    else
                    {
                        InvokeUnmanagedMethodWithTransition(targetMethod, stack, pFrame, callArgsAddress, returnValueAddress, callTarget);
                    }

                    break;
                }

                case INTOP_CALLDELEGATE_TAIL:
                case INTOP_CALLDELEGATE:
                {
                    isTailcall = (*ip == INTOP_CALLDELEGATE_TAIL);
                    returnOffset = ip[1];
                    callArgsOffset = ip[2];
                    methodSlot = ip[3];

                    int8_t* returnValueAddress = LOCAL_VAR_ADDR(returnOffset, int8_t);

                    // Used only for INTOP_CALLDELEGATE to allow removal of the delegate object from the argument list
                    int32_t sizeOfArgsUpto16ByteAlignment = 0;
                    if (*ip == INTOP_CALLDELEGATE)
                    {
                        sizeOfArgsUpto16ByteAlignment = ip[4];
                        ip += 5;
                    }
                    else
                    {
                        ip += 4;
                    }

                    DELEGATEREF* delegateObj = LOCAL_VAR_ADDR(callArgsOffset, DELEGATEREF);
                    NULL_CHECK(*delegateObj);
                    PCODE targetAddress = (*delegateObj)->GetMethodPtr();
                    DelegateEEClass *pDelClass = (DelegateEEClass*)(*delegateObj)->GetMethodTable()->GetClass();
                    if ((pDelClass->m_pInstRetBuffCallStub != NULL && pDelClass->m_pInstRetBuffCallStub->GetEntryPoint() == targetAddress) ||
                        (pDelClass->m_pStaticCallStub != NULL && pDelClass->m_pStaticCallStub->GetEntryPoint() == targetAddress))
                    {
                        // This implies that we're using a delegate shuffle thunk to strip off the first parameter to the method
                        // and call the actual underlying method. We allow for tail-calls to work and for greater efficiency in the
                        // interpreter by skipping the shuffle thunk and calling the actual target method directly.
                        PCODE actualTarget = (*delegateObj)->GetMethodPtrAux();

                        // Detect open virtual dispatch scenario
                        bool isOpenVirtual = false;
                        INTERFACE_DISPATCH_CACHED_OR_VSD(
                            if (actualTarget == (PCODE)CID_VirtualOpenDelegateDispatch)
                            {
                                isOpenVirtual = true;
                            },
                            if (VirtualCallStubManager::isStubStatic(actualTarget))
                            {
                                isOpenVirtual = true;
                            }
                        );

                        if (isOpenVirtual)
                        {
                            targetMethod = COMDelegate::GetMethodDescForOpenVirtualDelegate(*delegateObj);
                            targetMethod = CallWithSEHWrapper(
                                [&targetMethod, &callArgsOffset, &stack]() {
                                    return targetMethod->GetMethodDescOfVirtualizedCode(LOCAL_VAR_ADDR(callArgsOffset + INTERP_STACK_SLOT_SIZE, OBJECTREF), targetMethod->GetMethodTable());
                                });

                        }
                        else
                        {
                            targetMethod = NonVirtualEntry2MethodDesc(actualTarget);
                        }

                        PTR_InterpByteCodeStart targetIp;
                        if ((targetMethod) && (targetIp = targetMethod->GetInterpreterCode()) != NULL)
                        {
                            pFrame->ip = ip;
                            InterpMethod* pTargetMethod = targetIp->Method;
                            if (isTailcall)
                            {
                                // Move args from callArgsOffset to start of stack frame.
                                _ASSERTE(pTargetMethod->CheckIntegrity());
                                // It is safe to use memcpy because the source and destination are both on the interp stack, not in the GC heap.
                                // We need to use the target method's argsSize, not our argsSize, because tail calls (unlike CEE_JMP) can have a
                                //  different signature from the caller.
                                memcpy(pFrame->pStack, LOCAL_VAR_ADDR(callArgsOffset + INTERP_STACK_SLOT_SIZE, int8_t), pTargetMethod->argsSize);
                                // Reuse current stack frame. We discard the call insn's returnOffset because it's not important and tail calls are
                                //  required to be followed by a ret, so we know nothing is going to read from stack[returnOffset] after the call.
                                pFrame->ReInit(pFrame->pParent, targetIp, pFrame->pRetVal, pFrame->pStack);
                            }
                            else
                            {
                                // Shift args down by one slot to remove the delegate obj pointer.
                                // We need to preserve alignment of arguments that require 16-byte alignment.
                                // The sizeOfArgsUpto16ByteAlignment is the size of all the target method args starting at the first argument up to (but not including) the first argument that requires 16-byte alignment.
                                if (sizeOfArgsUpto16ByteAlignment != 0)
                                {
                                    memmove(LOCAL_VAR_ADDR(callArgsOffset, int8_t), LOCAL_VAR_ADDR(callArgsOffset + INTERP_STACK_SLOT_SIZE, int8_t), sizeOfArgsUpto16ByteAlignment);
                                }

                                if (sizeOfArgsUpto16ByteAlignment != pTargetMethod->argsSize)
                                {
                                    // There are arguments that require 16-byte alignment
                                    size_t firstAlignedTargetArgDstOffset = ALIGN_UP(sizeOfArgsUpto16ByteAlignment, INTERP_STACK_ALIGNMENT);
                                    size_t firstAlignedTargetArgSrcOffset = ALIGN_UP(INTERP_STACK_SLOT_SIZE + sizeOfArgsUpto16ByteAlignment, INTERP_STACK_ALIGNMENT);
                                    memmove(LOCAL_VAR_ADDR(callArgsOffset + firstAlignedTargetArgDstOffset, int8_t), LOCAL_VAR_ADDR(callArgsOffset + firstAlignedTargetArgSrcOffset, int8_t), pTargetMethod->argsSize - sizeOfArgsUpto16ByteAlignment);
                                }

                                // Allocate child frame.
                                InterpMethodContextFrame *pChildFrame = pFrame->pNext;
                                if (!pChildFrame)
                                {
                                    pChildFrame = (InterpMethodContextFrame*)alloca(sizeof(InterpMethodContextFrame));
                                    pChildFrame->pNext = NULL;
                                    pFrame->pNext = pChildFrame;
                                    SAVE_THE_LOWEST_SP;
                                }
                                pChildFrame->ReInit(pFrame, targetIp, returnValueAddress, LOCAL_VAR_ADDR(callArgsOffset, int8_t));
                                pFrame = pChildFrame;
                            }
                            // Set execution state for the new frame
                            pMethod = pFrame->startIp->Method;
                            _ASSERTE(pMethod->CheckIntegrity());
                            stack = pFrame->pStack;
                            ip = pFrame->startIp->GetByteCodes();
                            pThreadContext->pStackPointer = stack + pMethod->allocaSize;
                            break;
                        }
                    }

                    OBJECTREF targetMethodObj = (*delegateObj)->GetTarget();
                    LOCAL_VAR(callArgsOffset, OBJECTREF) = targetMethodObj;

                    if ((targetMethod = NonVirtualEntry2MethodDesc(targetAddress)) != NULL)
                    {
                        // In this case targetMethod holds a pointer to the MethodDesc that will be called by using targetMethodObj as
                        // the this pointer. This may be the final method (in the case of instance method delegates), or it may be a
                        // shuffle thunk, or multicast invoke method.
                        goto CALL_INTERP_METHOD;
                    }

                    // targetMethod holds a pointer to the Invoke method of the delegate, not the final actual target.
                    targetMethod = (MethodDesc*)pMethod->pDataItems[methodSlot];
                    int8_t* callArgsAddress = LOCAL_VAR_ADDR(callArgsOffset, int8_t);

                    // Save current execution state for when we return from called method
                    pFrame->ip = ip;

                    InvokeDelegateInvokeMethod(targetMethod, callArgsAddress, returnValueAddress, targetAddress, pInterpreterFrame->GetContinuationPtr());
                    break;
                }

                case INTOP_CALL_NULLCHECK:
                    // Check the target this pointer in the call for null
                    NULL_CHECK(LOCAL_VAR(ip[2], void*));
                    FALLTHROUGH;
                case INTOP_CALL_TAIL:
                case INTOP_CALL:
                {
                    isTailcall = (*ip == INTOP_CALL_TAIL);
                    returnOffset = ip[1];
                    callArgsOffset = ip[2];
                    methodSlot = ip[3];

                    ip += 4;
CALL_INTERP_SLOT:
                    targetMethod = (MethodDesc*)pMethod->pDataItems[methodSlot];
CALL_INTERP_METHOD:

                    int8_t* callArgsAddress = LOCAL_VAR_ADDR(callArgsOffset, int8_t);
                    int8_t* returnValueAddress = LOCAL_VAR_ADDR(returnOffset, int8_t);
                    // Save current execution state for when we return from called method
                    pFrame->ip = ip;

                    InterpByteCodeStart* targetIp = targetMethod->GetInterpreterCode();
                    if (targetIp == NULL)
                    {
                        if (!targetMethod->IsInterpreterCodePoisoned())
                        {
                            // This is an optimization to ensure that the stack walk will not have to search
                            // for the topmost frame in the current InterpExecMethod. It is not required
                            // for correctness, as the stack walk will find the topmost frame anyway. But it
                            // would need to seek through the frames to find it.
                            // An alternative approach would be to update the topmost frame during stack walk
                            // to make the probability that the next stack walk will need to search only a
                            // small subset of frames high.
                            pInterpreterFrame->SetTopInterpMethodContextFrame(pFrame);
                            GCX_PREEMP();

                            if (targetMethod->ShouldCallPrestub())
                            {
                                CallWithSEHWrapper(
                                    [&targetMethod]() {
                                        return targetMethod->DoPrestub(nullptr, CallerGCMode::Coop);
                                    });
                            }

                            targetIp = targetMethod->GetInterpreterCode();
                            if (targetIp == NULL)
                            {
                                // The prestub wasn't able to setup an interpreter code, so it will never be able to.
                                targetMethod->PoisonInterpreterCode();
                            }
                        }
                        if (targetIp == NULL)
                        {
                            // If we didn't get the interpreter code pointer setup, then this is a method we need to invoke as a compiled method.
                            // Interpreter-FIXME: Implement tailcall via helpers, see https://github.com/dotnet/runtime/blob/main/docs/design/features/tailcalls-with-helpers.md
                            InvokeManagedMethod(targetMethod, callArgsAddress, returnValueAddress, (PCODE)NULL, pInterpreterFrame->GetContinuationPtr());
                            break;
                        }
                    }

                    if (isTailcall)
                    {
                        // Move args from callArgsOffset to start of stack frame.
                        InterpMethod* pTargetMethod = targetIp->Method;
                        _ASSERTE(pTargetMethod->CheckIntegrity());
                        // It is safe to use memcpy because the source and destination are both on the interp stack, not in the GC heap.
                        // We need to use the target method's argsSize, not our argsSize, because tail calls (unlike CEE_JMP) can have a
                        //  different signature from the caller.
                        memcpy(pFrame->pStack, callArgsAddress, pTargetMethod->argsSize);
                        // Reuse current stack frame. We discard the call insn's returnOffset because it's not important and tail calls are
                        //  required to be followed by a ret, so we know nothing is going to read from stack[returnOffset] after the call.
                        pFrame->ReInit(pFrame->pParent, targetIp, pFrame->pRetVal, pFrame->pStack);
                    }
                    else
                    {
                        // Save current execution state for when we return from called method
                        pFrame->ip = ip;

                        // Allocate child frame.
                        {
                            InterpMethodContextFrame *pChildFrame = pFrame->pNext;
                            if (!pChildFrame)
                            {
                                pChildFrame = (InterpMethodContextFrame*)alloca(sizeof(InterpMethodContextFrame));
                                pChildFrame->pNext = NULL;
                                pFrame->pNext = pChildFrame;
                                SAVE_THE_LOWEST_SP;
                            }
                            pChildFrame->ReInit(pFrame, targetIp, returnValueAddress, callArgsAddress);
                            pFrame = pChildFrame;
                        }
                        assert (((size_t)pFrame->pStack % INTERP_STACK_ALIGNMENT) == 0);
                    }

                    // Set execution state for the new frame
                    pMethod = pFrame->startIp->Method;
                    _ASSERTE(pMethod->CheckIntegrity());
                    stack = pFrame->pStack;
                    ip = pFrame->startIp->GetByteCodes();

                    if (stack + pMethod->allocaSize > pThreadContext->pStackEnd)
                    {
                        pFrame->ip = ip;
                        HandleInterpreterStackOverflow(pInterpreterFrame);
                        UNREACHABLE();
                    }
                    pThreadContext->pStackPointer = stack + pMethod->allocaSize;
                    break;
                }
                case INTOP_NEWOBJ_GENERIC:
                {
                    isTailcall = false;
                    returnOffset = ip[1];
                    callArgsOffset = ip[2];
                    methodSlot = ip[4];
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[5]];
                    MethodTable *pMTNewObj = (MethodTable*)DoGenericLookup(LOCAL_VAR(ip[3], void*), pLookup);

                    OBJECTREF objRef = AllocateObject(pMTNewObj);

                    // This is return value
                    LOCAL_VAR(returnOffset, OBJECTREF) = objRef;
                    // Set `this` arg for ctor call
                    LOCAL_VAR (callArgsOffset, OBJECTREF) = objRef;
                    ip += 6;

                    goto CALL_INTERP_SLOT;
                }
                case INTOP_NEWOBJ:
                {
                    isTailcall = false;
                    returnOffset = ip[1];
                    callArgsOffset = ip[2];
                    methodSlot = ip[3];

                    OBJECTREF objRef = AllocateObject((MethodTable*)pMethod->pDataItems[ip[4]]);

                    // This is return value
                    LOCAL_VAR(returnOffset, OBJECTREF) = objRef;
                    // Set `this` arg for ctor call
                    LOCAL_VAR (callArgsOffset, OBJECTREF) = objRef;
                    ip += 5;

                    goto CALL_INTERP_SLOT;
                }
                case INTOP_NEWMDARR:
                {
                    LOCAL_VAR(ip[1], OBJECTREF) = CreateMultiDimArray((MethodTable*)pMethod->pDataItems[ip[3]], stack, ip[2], ip[4]);
                    ip += 5;
                    break;
                }
                case INTOP_NEWMDARR_GENERIC:
                {
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[4]];
                    MethodTable *pMTArray = (MethodTable*)DoGenericLookup(LOCAL_VAR(ip[3], void*), pLookup);

                    LOCAL_VAR(ip[1], OBJECTREF) = CreateMultiDimArray(pMTArray, stack, ip[2], ip[5]);
                    ip += 6;
                    break;
                }
                case INTOP_NEWOBJ_VT:
                {
                    isTailcall = false;
                    returnOffset = ip[1];
                    callArgsOffset = ip[2];
                    methodSlot = ip[3];

                    int32_t vtSize = ip[4];
                    void *vtThis = LOCAL_VAR_ADDR(returnOffset, void);

                    memset(vtThis, 0, vtSize);

                    // pass the address of the valuetype
                    LOCAL_VAR(callArgsOffset, void*) = vtThis;

                    ip += 5;
                    goto CALL_INTERP_SLOT;
                }
                case INTOP_ZEROBLK_IMM:
                {
                    void* dst = LOCAL_VAR(ip[1], void*);
                    NULL_CHECK(dst);
                    memset(dst, 0, ip[2]);
                    ip += 3;
                    break;
                }
                case INTOP_CPBLK:
                {
                    void* dst = LOCAL_VAR(ip[1], void*);
                    void* src = LOCAL_VAR(ip[2], void*);
                    uint32_t size = LOCAL_VAR(ip[3], uint32_t);
                    if (size && (!dst || !src))
                        COMPlusThrow(kNullReferenceException);
                    else
                        memcpyNoGCRefs(dst, src, size);
                    ip += 4;
                    break;
                }
                case INTOP_INITBLK:
                {
                    void* dst = LOCAL_VAR(ip[1], void*);
                    uint8_t value = LOCAL_VAR(ip[2], uint8_t);
                    uint32_t size = LOCAL_VAR(ip[3], uint32_t);
                    if (size && !dst)
                        COMPlusThrow(kNullReferenceException);
                    else
                        memset(dst, value, size);
                    ip += 4;
                    break;
                }
                case INTOP_LOCALLOC:
                {
                    size_t len = LOCAL_VAR(ip[2], size_t);
                    void* pMemory = NULL;

                    if (len > 0)
                    {
                        pMemory = pThreadContext->frameDataAllocator.Alloc(pFrame, len);
                        if (pMemory == NULL)
                        {
                            COMPlusThrowOM();
                        }
                        if (pMethod->initLocals)
                        {
                            memset(pMemory, 0, len);
                        }
                    }

                    LOCAL_VAR(ip[1], void*) = pMemory;
                    ip += 3;
                    break;
                }
                case INTOP_THROW:
                {
                    OBJECTREF throwable = LOCAL_VAR(ip[1], OBJECTREF);
                    if (!throwable)
                    {
                        EEException ex(kNullReferenceException);
                        throwable = ex.CreateThrowable();
                    }
                    else
                    {
                        NormalizeThrownObject(&throwable);
                    }

                    pInterpreterFrame->SetIsFaulting(true);
                    DispatchManagedException(throwable);
                    UNREACHABLE();
                    break;
                }
                case INTOP_RETHROW:
                {
                    pInterpreterFrame->SetIsFaulting(true);
                    DispatchRethrownManagedException();
                    UNREACHABLE();
                    break;
                }
                case INTOP_LOAD_EXCEPTION:
                    // This opcode loads the exception object coming from a catch / filter funclet caller to a variable.
                    _ASSERTE(pExceptionClauseArgs != NULL);
                    LOCAL_VAR(ip[1], OBJECTREF) = pExceptionClauseArgs->throwable;
                    ip += 2;
                    break;
                case INTOP_UNBOX_ANY:
                {
                    int opcode = *ip;
                    int dreg = ip[1];
                    int sreg = ip[2];
                    MethodTable *pMT = (MethodTable*)pMethod->pDataItems[ip[4]];
                    Object *src = LOCAL_VAR(sreg, Object*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_BOX_UNBOX helper = GetPossiblyIndirectHelper<HELPER_FTN_BOX_UNBOX>(pMethod, ip[3], &pILTargetMethod);
                    if (pILTargetMethod != NULL)
                    {
                        callArgsOffset = pMethod->allocaSize;
                        returnOffset = dreg;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = pMT;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = src;

                        targetMethod = pILTargetMethod;
                        ip += 5;

                        goto CALL_INTERP_METHOD;
                    }

                    // private static ref byte Unbox(MethodTable* toTypeHnd, object obj)
                    LOCAL_VAR(dreg, void*) = helper(pMT, src);

                    ip += 5;
                    break;
                }
                case INTOP_UNBOX_END:
                {
                    MethodTable *pMT = (MethodTable*)pMethod->pDataItems[ip[3]];
                    void *dest = LOCAL_VAR_ADDR(ip[1], void);
                    void *src = LOCAL_VAR(ip[2], void*);
                    NULL_CHECK(dest);
                    CopyValueClassUnchecked(dest, src, pMT);

                    ip += 4;
                    break;
                }
                case INTOP_UNBOX_ANY_GENERIC:
                {
                    int opcode = *ip;
                    int dreg = ip[1];
                    int sreg = ip[3];
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[5]];
                    MethodTable *pMTBoxedObj = (MethodTable*)DoGenericLookup(LOCAL_VAR(ip[2], void*), pLookup);
                    Object *src = LOCAL_VAR(sreg, Object*);

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_BOX_UNBOX helper = GetPossiblyIndirectHelper<HELPER_FTN_BOX_UNBOX>(pMethod, ip[4], &pILTargetMethod);

                    if (pILTargetMethod != NULL)
                    {
                        callArgsOffset = pMethod->allocaSize;
                        returnOffset = dreg;

                        // Pass arguments to the target method
                        LOCAL_VAR(callArgsOffset, void*) = pMTBoxedObj;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, void*) = src;

                        targetMethod = pILTargetMethod;
                        ip += 6;

                        goto CALL_INTERP_METHOD;
                    }

                    // private static ref byte Unbox(MethodTable* toTypeHnd, object obj)
                    LOCAL_VAR(dreg, void*) = helper(pMTBoxedObj, src);

                    ip += 6;
                    break;
                }
                case INTOP_UNBOX_END_GENERIC:
                {
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[4]];
                    MethodTable *pMT = (MethodTable*)DoGenericLookup(LOCAL_VAR(ip[2], void*), pLookup);
                    void *dest = LOCAL_VAR_ADDR(ip[1], void);
                    void *src = LOCAL_VAR(ip[3], void*);
                    NULL_CHECK(dest);
                    CopyValueClassUnchecked(dest, src, pMT->IsNullable() ? pMT->GetInstantiation()[0].AsMethodTable() : pMT);

                    ip += 5;
                    break;
                }
                case INTOP_NEWARR:
                {
                    int32_t length = LOCAL_VAR(ip[2], int32_t);

                    MethodTable* arrayClsHnd = (MethodTable*)pMethod->pDataItems[ip[3]];
                    HELPER_FTN_NEWARR helper = GetPossiblyIndirectHelper<HELPER_FTN_NEWARR>(pMethod, ip[4]);

                    Object* arr = helper(arrayClsHnd, (intptr_t)length);
                    LOCAL_VAR(ip[1], OBJECTREF) = ObjectToOBJECTREF(arr);

                    ip += 5;
                    break;
                }
                case INTOP_NEWARR_GENERIC:
                {
                    int32_t length = LOCAL_VAR(ip[3], int32_t);

                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[5]];
                    MethodTable *arrayClsHnd = (MethodTable*)DoGenericLookup(LOCAL_VAR(ip[2], void*), pLookup);

                    HELPER_FTN_NEWARR helper = GetPossiblyIndirectHelper<HELPER_FTN_NEWARR>(pMethod, ip[4]);

                    Object* arr = helper(arrayClsHnd, (intptr_t)length);
                    LOCAL_VAR(ip[1], OBJECTREF) = ObjectToOBJECTREF(arr);

                    ip += 6;
                    break;
                }
#define LDELEM(dtype,etype)                                                    \
do {                                                                           \
    BASEARRAYREF arrayRef = LOCAL_VAR(ip[2], BASEARRAYREF);                    \
    if (arrayRef == NULL)                                                      \
        COMPlusThrow(kNullReferenceException);                                 \
                                                                               \
    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);                  \
    uint32_t len = arr->GetNumComponents();                                    \
    uint32_t idx = (uint32_t)LOCAL_VAR(ip[3], int32_t);                        \
    if (idx >= len)                                                            \
        COMPlusThrow(kIndexOutOfRangeException);                               \
                                                                               \
    uint8_t* pData = arr->GetDataPtr();                                        \
    etype* pElem = reinterpret_cast<etype*>(pData + idx * sizeof(etype));      \
                                                                               \
    LOCAL_VAR(ip[1], dtype) = *pElem;                                          \
    ip += 4;                                                                   \
} while (0)
                case INTOP_LDELEM_I1:
                {
                    LDELEM(int32_t, int8_t);
                    break;
                }
                case INTOP_LDELEM_U1:
                {
                    LDELEM(int32_t, uint8_t);
                    break;
                }
                case INTOP_LDELEM_I2:
                {
                    LDELEM(int32_t, int16_t);
                    break;
                }
                case INTOP_LDELEM_U2:
                {
                    LDELEM(int32_t, uint16_t);
                    break;
                }
                case INTOP_LDELEM_I4:
                {
                    LDELEM(int32_t, int32_t);
                    break;
                }
                case INTOP_LDELEM_I8:
                {
                    LDELEM(int64_t, int64_t);
                    break;
                }
                case INTOP_LDELEM_R4:
                {
                    LDELEM(float, float);
                    break;
                }
                case INTOP_LDELEM_R8:
                {
                    LDELEM(double, double);
                    break;
                }
#undef LDELEM
                case INTOP_LDELEM_REF:
                {
                    BASEARRAYREF arrayRef = LOCAL_VAR(ip[2], BASEARRAYREF);
                    if (arrayRef == NULL)
                        COMPlusThrow(kNullReferenceException);

                    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                    uint32_t len = arr->GetNumComponents();
                    uint32_t idx = (uint32_t)LOCAL_VAR(ip[3], int32_t);
                    if (idx >= len)
                        COMPlusThrow(kIndexOutOfRangeException);

                    uint8_t* pData = arr->GetDataPtr();
                    OBJECTREF elemRef = ObjectToOBJECTREF(*(Object**)(pData + idx * sizeof(OBJECTREF)));
                    LOCAL_VAR(ip[1], OBJECTREF) = elemRef;
                    ip += 4;
                    break;
                }
                case INTOP_LDELEM_VT:
                {
                    BASEARRAYREF arrayRef = LOCAL_VAR(ip[2], BASEARRAYREF);
                    if (arrayRef == NULL)
                        COMPlusThrow(kNullReferenceException);

                    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                    uint32_t len = arr->GetNumComponents();
                    uint32_t idx = (uint32_t)LOCAL_VAR(ip[3], int32_t);
                    if (idx >= len)
                        COMPlusThrow(kIndexOutOfRangeException);

                    uint8_t* pData = arr->GetDataPtr();
                    size_t componentSize = arr->GetMethodTable()->GetComponentSize();
                    void* elemAddr = pData + idx * componentSize;
                    MethodTable* pElemMT = arr->GetArrayElementTypeHandle().AsMethodTable();
                    CopyValueClassUnchecked(LOCAL_VAR_ADDR(ip[1], void), elemAddr, pElemMT);
                    ip += 5;
                    break;
                }
#define STELEM(dtype,etype)                                                    \
do {                                                                           \
    BASEARRAYREF arrayRef = LOCAL_VAR(ip[1], BASEARRAYREF);                    \
    if (arrayRef == NULL)                                                      \
        COMPlusThrow(kNullReferenceException);                                 \
                                                                               \
    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);                  \
    uint32_t len = arr->GetNumComponents();                                    \
    uint32_t idx = (uint32_t)LOCAL_VAR(ip[2], int32_t);                        \
    if (idx >= len)                                                            \
        COMPlusThrow(kIndexOutOfRangeException);                               \
                                                                               \
    uint8_t* pData = arr->GetDataPtr();                                        \
    etype* pElem = reinterpret_cast<etype*>(pData + idx * sizeof(etype));      \
                                                                               \
    *pElem = (etype)LOCAL_VAR(ip[3], dtype);                                   \
    ip += 4;                                                                   \
} while (0)
                case INTOP_STELEM_I1:
                {
                    STELEM(int32_t, int8_t);
                    break;
                }
                case INTOP_STELEM_I2:
                {
                    STELEM(int32_t, int16_t);
                    break;
                }
                case INTOP_STELEM_I4:
                {
                    STELEM(int32_t, int32_t);
                    break;
                }
                case INTOP_STELEM_I8:
                {
                    STELEM(int64_t, int64_t);
                    break;
                }
                case INTOP_STELEM_U1:
                {
                    STELEM(int32_t, uint8_t);
                    break;
                }
                case INTOP_STELEM_U2:
                {
                    STELEM(int32_t, uint16_t);
                    break;
                }
                case INTOP_STELEM_R4:
                {
                    STELEM(float, float);
                    break;
                }
                case INTOP_STELEM_R8:
                {
                    STELEM(double, double);
                    break;
                }
#undef STELEM
                case INTOP_STELEM_REF:
                {
                    BASEARRAYREF arrayRef = LOCAL_VAR(ip[1], BASEARRAYREF);
                    if (arrayRef == NULL)
                        COMPlusThrow(kNullReferenceException);

                    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                    uint32_t len = arr->GetNumComponents();
                    uint32_t idx = (uint32_t)LOCAL_VAR(ip[2], int32_t);
                    if (idx >= len)
                        COMPlusThrow(kIndexOutOfRangeException);

                    OBJECTREF elemRef = LOCAL_VAR(ip[3], OBJECTREF);

                    if (elemRef != NULL)
                    {
                        TypeHandle arrayElemType = arr->GetArrayElementTypeHandle();
                        if (!ObjIsInstanceOf(OBJECTREFToObject(elemRef), arrayElemType.AsMethodTable()))
                            COMPlusThrow(kArrayTypeMismatchException);

                        // ObjIsInstanceOf can trigger GC, so the object references have to be re-fetched
                        arrayRef = LOCAL_VAR(ip[1], BASEARRAYREF);
                        arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                        elemRef = LOCAL_VAR(ip[3], OBJECTREF);
                    }

                    uint8_t* pData = arr->GetDataPtr();
                    SetObjectReferenceUnchecked((OBJECTREF*)(pData + idx * sizeof(OBJECTREF)), elemRef);
                    ip += 4;
                    break;
                }
                case INTOP_STELEM_VT:
                {
                    BASEARRAYREF arrayRef = LOCAL_VAR(ip[1], BASEARRAYREF);
                    if (arrayRef == NULL)
                        COMPlusThrow(kNullReferenceException);

                    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                    uint32_t len = arr->GetNumComponents();
                    uint32_t idx = (uint32_t)LOCAL_VAR(ip[2], int32_t);
                    if (idx >= len)
                        COMPlusThrow(kIndexOutOfRangeException);

                    uint8_t* pData = arr->GetDataPtr();
                    size_t elemSize = ip[4];
                    void* elemAddr = pData + idx * elemSize;
                    MethodTable* pMT = (MethodTable*)pMethod->pDataItems[ip[5]];

                    CopyValueClassUnchecked(elemAddr, LOCAL_VAR_ADDR(ip[3], void), pMT);
                    ip += 6;
                    break;
                }
                case INTOP_STELEM_VT_NOREF:
                {
                    BASEARRAYREF arrayRef = LOCAL_VAR(ip[1], BASEARRAYREF);
                    if (arrayRef == NULL)
                        COMPlusThrow(kNullReferenceException);

                    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                    uint32_t len = arr->GetNumComponents();
                    uint32_t idx = (uint32_t)LOCAL_VAR(ip[2], int32_t);
                    if (idx >= len)
                        COMPlusThrow(kIndexOutOfRangeException);

                    uint8_t* pData = arr->GetDataPtr();
                    size_t elemSize = ip[4];
                    void* elemAddr = pData + idx * elemSize;

                    memcpyNoGCRefs(elemAddr, LOCAL_VAR_ADDR(ip[3], void), elemSize);
                    ip += 5;
                    break;
                }
                case INTOP_LDELEMA:
                {
                    BASEARRAYREF arrayRef = LOCAL_VAR(ip[2], BASEARRAYREF);
                    if (arrayRef == NULL)
                        COMPlusThrow(kNullReferenceException);

                    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                    uint32_t len = arr->GetNumComponents();
                    uint32_t idx = (uint32_t)LOCAL_VAR(ip[3], int32_t);
                    if (idx >= len)
                        COMPlusThrow(kIndexOutOfRangeException);

                    uint8_t* pData = arr->GetDataPtr();
                    size_t elemSize = ip[4];
                    void* elemAddr = pData + idx * elemSize;
                    LOCAL_VAR(ip[1], void*) = elemAddr;
                    ip += 5;
                    break;
                }
                case INTOP_LDELEMA_REF:
                {
                    BASEARRAYREF arrayRef = LOCAL_VAR(ip[2], BASEARRAYREF);
                    if (arrayRef == NULL)
                        COMPlusThrow(kNullReferenceException);

                    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                    uint32_t len = arr->GetNumComponents();
                    uint32_t idx = (uint32_t)LOCAL_VAR(ip[3], int32_t);
                    if (idx >= len)
                        COMPlusThrow(kIndexOutOfRangeException);

                    uint8_t* pData = arr->GetDataPtr();
                    void* elemAddr = pData + idx * sizeof(void*);

                    MethodTable* arrayElemMT = arr->GetArrayElementTypeHandle().AsMethodTable();
                    MethodTable* expectedMT = (MethodTable*)pMethod->pDataItems[ip[4]];
                    if (arrayElemMT != expectedMT)
                    {
                        COMPlusThrow(kArrayTypeMismatchException);
                    }

                    LOCAL_VAR(ip[1], void*) = elemAddr;
                    ip += 5;
                    break;
                }

                case INTOP_LDELEMA_REF_GENERIC:
                {
                    BASEARRAYREF arrayRef = LOCAL_VAR(ip[2], BASEARRAYREF);
                    if (arrayRef == NULL)
                        COMPlusThrow(kNullReferenceException);

                    ArrayBase* arr = (ArrayBase*)OBJECTREFToObject(arrayRef);
                    uint32_t len = arr->GetNumComponents();
                    uint32_t idx = (uint32_t)LOCAL_VAR(ip[3], int32_t);
                    if (idx >= len)
                        COMPlusThrow(kIndexOutOfRangeException);

                    uint8_t* pData = arr->GetDataPtr();
                    void* elemAddr = pData + idx * sizeof(void*);

                    MethodTable* arrayElemMT = arr->GetArrayElementTypeHandle().AsMethodTable();
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[5]];
                    MethodTable* expectedMT = (MethodTable*)DoGenericLookup(LOCAL_VAR(ip[4], void*), pLookup);
                    if (arrayElemMT != expectedMT)
                    {
                        COMPlusThrow(kArrayTypeMismatchException);
                    }

                    LOCAL_VAR(ip[1], void*) = elemAddr;
                    ip += 6;
                    break;
                }

                case INTOP_CONV_NI:
                {
                    NULL_CHECK(LOCAL_VAR(ip[2], void*));
                    int64_t index = LOCAL_VAR(ip[3], int64_t);
                    if ((index < 0) || (index > UINT_MAX))
                    {
                        COMPlusThrow(kIndexOutOfRangeException);
                    }
                    LOCAL_VAR(ip[1], int64_t) = index;
                    ip += 4;
                    break;
                }

                case INTOP_GENERICLOOKUP:
                {
                    int dreg = ip[1];
                    InterpGenericLookup *pLookup = (InterpGenericLookup*)&pMethod->pDataItems[ip[3]];
                    void* result = DoGenericLookup(LOCAL_VAR(ip[2], void*), pLookup);
                    LOCAL_VAR(dreg, void*) = result;
                    ip += 4;
                    break;
                }

                case INTOP_COMPARE_EXCHANGE_U1:
                {
                    uint8_t* dst = LOCAL_VAR(ip[2], uint8_t*);
                    NULL_CHECK(dst);
                    uint8_t newValue = LOCAL_VAR(ip[3], uint8_t);
                    uint8_t comparand = LOCAL_VAR(ip[4], uint8_t);
                    // Since our Interlocked API doesn't support 8-bit exchange, we implement this via a loop and CompareExchange
                    // This is not optimal, but 8-bit exchange is not a common operation.
                    ULONG* dstUINT32Aligned = (ULONG*)(uint8_t*)((size_t)dst & ~3);
                    int32_t shift = ((size_t)dst & 3) * 8;
                    ULONG mask = ~(((ULONG)0xFF) << shift);
                    do
                    {
                        ULONG oldULONG = *dstUINT32Aligned;
                        ULONG newValueUINT32 = ((uint32_t)newValue << shift) | (oldULONG & mask);
                        ULONG comparandUINT32 = ((uint32_t)comparand << shift) | (oldULONG & mask);
                        ULONG compareExchangeResult = InterlockedCompareExchangeT(dstUINT32Aligned, newValueUINT32, comparandUINT32);
                        if (((compareExchangeResult & ~mask) == (comparandUINT32 & ~mask)) && (compareExchangeResult != comparandUINT32))
                        {
                            // Compare exchange failed, but doesn't look like it, since some other bytes changed
                            // rerun the CompareExchange
                            continue;
                        }
                        // CompareExchange succeeded
                        LOCAL_VAR(ip[1], uint32_t) = (uint32_t)(uint8_t)(oldULONG >> shift);
                        break;
                    } while (true);
                    ip += 5;
                    break;
                }

                case INTOP_COMPARE_EXCHANGE_U2:
                {
                    uint16_t* dst = LOCAL_VAR(ip[2], uint16_t*);
                    NULL_CHECK(dst);
                    uint16_t newValue = LOCAL_VAR(ip[3], uint16_t);
                    uint16_t comparand = LOCAL_VAR(ip[4], uint16_t);
                    // Since our Interlocked API doesn't support 16-bit exchange, we implement this via a loop and CompareExchange
                    // This is not optimal, but 16-bit exchange is not a common operation.
                    ULONG* dstUINT32Aligned = (ULONG*)(uint16_t*)((size_t)dst & ~3);
                    int32_t shift = ((size_t)dst & 3) * 8;
                    if (shift & 1)
                    {
                        // Attempt to do 16-bit exchange on 2-byte unaligned address
                        COMPlusThrow(kAccessViolationException);
                    }
                    ULONG mask = ~(((ULONG)0xFFFF) << shift);
                    do
                    {
                        ULONG oldULONG = *dstUINT32Aligned;
                        ULONG newValueUINT32 = ((uint32_t)newValue << shift) | (oldULONG & mask);
                        ULONG comparandUINT32 = ((uint32_t)comparand << shift) | (oldULONG & mask);
                        ULONG compareExchangeResult = InterlockedCompareExchangeT(dstUINT32Aligned, newValueUINT32, comparandUINT32);
                        if (((compareExchangeResult & ~mask) == (comparandUINT32 & ~mask)) && (compareExchangeResult != comparandUINT32))
                        {
                            // Compare exchange failed, but doesn't look like it, since some other bytes changed
                            // rerun the CompareExchange
                            continue;
                        }
                        // CompareExchange succeeded
                        LOCAL_VAR(ip[1], uint32_t) = (uint32_t)(uint16_t)(oldULONG >> shift);
                        break;
                    } while (true);
                    ip += 5;
                    break;
                }


#define COMPARE_EXCHANGE(type)                                          \
do                                                                      \
{                                                                       \
    type* dst = (type*)LOCAL_VAR(ip[2], void*);                         \
    NULL_CHECK(dst);                                                    \
    type newValue = LOCAL_VAR(ip[3], type);                             \
    type comparand = LOCAL_VAR(ip[4], type);                            \
    type old = InterlockedCompareExchangeT(dst, newValue, comparand);   \
    LOCAL_VAR(ip[1], type) = old;                                       \
    ip += 5;                                                            \
} while (0)
                case INTOP_COMPARE_EXCHANGE_I4:
                {
                    COMPARE_EXCHANGE(int32_t);
                    break;
                }

                case INTOP_COMPARE_EXCHANGE_I8:
                {
                    COMPARE_EXCHANGE(int64_t);
                    break;
                }
#undef COMPARE_EXCHANGE

#define EXCHANGE(type)                                                  \
do                                                                      \
{                                                                       \
    type* dst = LOCAL_VAR(ip[2], type*);                                \
    NULL_CHECK(dst);                                                    \
    type newValue = LOCAL_VAR(ip[3], type);                             \
    type old = InterlockedExchangeT(dst, newValue);                     \
    LOCAL_VAR(ip[1], type) = old;                                       \
    ip += 4;                                                            \
} while (0)

                case INTOP_EXCHANGE_U1:
                {
                    uint8_t* dst = LOCAL_VAR(ip[2], uint8_t*);
                    NULL_CHECK(dst);
                    uint8_t newValue = LOCAL_VAR(ip[3], uint8_t);
                    // Since our Interlocked API doesn't support 8-bit exchange, we implement this via a loop and CompareExchange
                    // This is not optimal, but 8-bit exchange is not a common operation.
                    ULONG* dstUINT32Aligned = (ULONG*)(uint8_t*)((size_t)dst & ~3);
                    int32_t shift = ((size_t)dst & 3) * 8;
                    ULONG mask = ~(((ULONG)0xFF) << shift);
                    do
                    {
                        ULONG oldULONG = *dstUINT32Aligned;
                        ULONG newValueUINT32 = ((uint32_t)newValue << shift) | (oldULONG & mask);
                        if (InterlockedCompareExchangeT(dstUINT32Aligned, newValueUINT32, oldULONG) == oldULONG)
                        {
                            LOCAL_VAR(ip[1], uint32_t) = (uint32_t)(uint8_t)(oldULONG >> shift);
                            break;
                        }
                    } while (true);
                    ip += 4;
                    break;
                }

                case INTOP_EXCHANGE_U2:
                {
                    uint16_t* dst = LOCAL_VAR(ip[2], uint16_t*);
                    NULL_CHECK(dst);
                    uint16_t newValue = LOCAL_VAR(ip[3], uint16_t);
                    // Since our Interlocked API doesn't support 16-bit exchange, we implement this via a loop and CompareExchange
                    // This is not optimal, but 16-bit exchange is not a common operation.
                    ULONG* dstUINT32Aligned = (ULONG*)(uint16_t*)((size_t)dst & ~3);
                    int32_t shift = ((size_t)dst & 3) * 8;
                    if (shift & 1)
                    {
                        // Attempt to do 16-bit exchange on 2-byte unaligned address
                        COMPlusThrow(kAccessViolationException);
                    }
                    ULONG mask = ~(((ULONG)0xFFFF) << shift);
                    do
                    {
                        ULONG oldULONG = *dstUINT32Aligned;
                        ULONG newValueUINT32 = ((uint32_t)newValue << shift) | (oldULONG & mask);
                        if (InterlockedCompareExchangeT(dstUINT32Aligned, newValueUINT32, oldULONG) == oldULONG)
                        {
                            LOCAL_VAR(ip[1], uint32_t) = (uint32_t)(uint16_t)(oldULONG >> shift);
                            break;
                        }
                    } while (true);
                    ip += 4;
                    break;
                }

                case INTOP_EXCHANGE_I4:
                {
                    EXCHANGE(int32_t);
                    break;
                }

                case INTOP_EXCHANGE_I8:
                {
                    EXCHANGE(int64_t);
                    break;
                }
#undef EXCHANGE

                case INTOP_EXCHANGEADD_I4:
                {
                    LONG* dst = LOCAL_VAR(ip[2], LONG*);
                    NULL_CHECK(dst);
                    LONG newValue = LOCAL_VAR(ip[3], LONG);
                    LONG old = InterlockedExchangeAdd(dst, newValue);
                    LOCAL_VAR(ip[1], LONG) = old;
                    ip += 4;
                    break;
                }

                case INTOP_EXCHANGEADD_I8:
                {
                    LONG64* dst = LOCAL_VAR(ip[2], LONG64*);
                    NULL_CHECK(dst);
                    LONG64 newValue = LOCAL_VAR(ip[3], LONG64);
                    LONG64 old = InterlockedExchangeAdd64(dst, newValue);
                    LOCAL_VAR(ip[1], LONG64) = old;
                    ip += 4;
                    break;
                }

                case INTOP_SQRT_R4:
                {
                    float value = LOCAL_VAR(ip[2], float);
                    LOCAL_VAR(ip[1], float) = sqrtf(value);
                    ip += 3;
                    break;
                }

                case INTOP_SQRT_R8:
                {
                    double value = LOCAL_VAR(ip[2], double);
                    LOCAL_VAR(ip[1], double) = sqrt(value);
                    ip += 3;
                    break;
                }

                case INTOP_CALL_FINALLY:
                {
                    const int32_t* targetIp = ip + ip[1];
                    // Save current execution state for when we return from called method
                    pFrame->ip = ip + 2;

                    // Allocate child frame.
                    {
                        InterpMethodContextFrame *pChildFrame = pFrame->pNext;
                        if (!pChildFrame)
                        {
                            pChildFrame = (InterpMethodContextFrame*)alloca(sizeof(InterpMethodContextFrame));
                            pChildFrame->pNext = NULL;
                            pFrame->pNext = pChildFrame;
                            SAVE_THE_LOWEST_SP;
                        }
                        // Set the frame to the same values as the caller frame.
                        pChildFrame->ReInit(pFrame, pFrame->startIp, pFrame->pRetVal, pFrame->pStack);
                        pFrame = pChildFrame;
                    }
                    assert (((size_t)pFrame->pStack % INTERP_STACK_ALIGNMENT) == 0);

                    // Set execution state for the new frame
                    ip = targetIp;
                    break;
                }
                case INTOP_LEAVE_FILTER:
                    *(int64_t*)pFrame->pRetVal = LOCAL_VAR(ip[1], int32_t);
                    goto EXIT_FRAME;
                case INTOP_LEAVE_CATCH:
                    *(const int32_t**)pFrame->pRetVal = ip + ip[1];
                    goto EXIT_FRAME;
                case INTOP_THROW_PNSE:
                    COMPlusThrow(kPlatformNotSupportedException);
                    break;

                case INTOP_HANDLE_CONTINUATION_GENERIC:
                case INTOP_HANDLE_CONTINUATION:
                {
                    int32_t helperOffset = *ip == INTOP_HANDLE_CONTINUATION_GENERIC ? 4 : 3;
                    int32_t ipAdjust = *ip == INTOP_HANDLE_CONTINUATION_GENERIC ? 5 : 4;
                    int32_t suspendDataOffset = *ip == INTOP_HANDLE_CONTINUATION_GENERIC ? 3 : 2;
                    InterpAsyncSuspendData *pAsyncSuspendData = (InterpAsyncSuspendData*)pMethod->pDataItems[ip[suspendDataOffset]];

                    // Zero out locals that need zeroing
                    InterpIntervalMapEntry *pZeroLocalsEntry = pAsyncSuspendData->zeroedLocalsIntervals;
                    while (pZeroLocalsEntry->countBytes != 0)
                    {
                        memset(LOCAL_VAR_ADDR(pZeroLocalsEntry->startOffset, uint8_t), 0, pZeroLocalsEntry->countBytes);
                        pZeroLocalsEntry++;
                    }

                    if (pInterpreterFrame->GetContinuation() == NULL)
                    {
                        // No continuation to handle.
                        LOCAL_VAR(ip[1], void*) = NULL; // We don't allocate a continuation here
                        ip += ipAdjust; // (4 or 5 for this opcode)
                        break;
                    }
                    MethodTable *pContinuationType = pAsyncSuspendData->continuationTypeHnd;

                    MethodDesc *pILTargetMethod = NULL;
                    HELPER_FTN_P_PP helperFtn = NULL;
                    HELPER_FTN_P_PPIP helperFtnGeneric = NULL;
                    if (*ip == INTOP_HANDLE_CONTINUATION_GENERIC)
                    {
                        helperFtnGeneric = GetPossiblyIndirectHelper<HELPER_FTN_P_PPIP>(pMethod, ip[helperOffset], &pILTargetMethod);
                    }
                    else
                    {
                        helperFtn = GetPossiblyIndirectHelper<HELPER_FTN_P_PP>(pMethod, ip[helperOffset], &pILTargetMethod);
                    }
                    if (pILTargetMethod != NULL)
                    {
                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass argument to the target method
                        LOCAL_VAR(callArgsOffset, OBJECTREF) = pInterpreterFrame->GetContinuation();
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, MethodTable*) = pContinuationType;
                        if (*ip == INTOP_HANDLE_CONTINUATION_GENERIC)
                        {
                            LOCAL_VAR(callArgsOffset + 2 * INTERP_STACK_SLOT_SIZE, int32_t) = pAsyncSuspendData->keepAliveOffset;
                            LOCAL_VAR(callArgsOffset + 3 * INTERP_STACK_SLOT_SIZE, uintptr_t) = LOCAL_VAR(ip[2], uintptr_t);
                        }

                        pInterpreterFrame->SetContinuation(NULL);
                        targetMethod = pILTargetMethod;
                        ip += ipAdjust;
                        goto CALL_INTERP_METHOD;
                    }

                    OBJECTREF chainedContinuation = pInterpreterFrame->GetContinuation();
                    pInterpreterFrame->SetContinuation(NULL);
                    OBJECTREF* pDest = LOCAL_VAR_ADDR(ip[1], OBJECTREF);
                    if (*ip == INTOP_HANDLE_CONTINUATION_GENERIC)
                    {
                        uintptr_t context = LOCAL_VAR(ip[2], uintptr_t);
                        ip += ipAdjust;
                        *pDest = ObjectToOBJECTREF((Object*)helperFtnGeneric(OBJECTREFToObject(chainedContinuation), pContinuationType, pAsyncSuspendData->keepAliveOffset, (void*)context));
                    }
                    else
                    {
                        ip += ipAdjust;
                        *pDest = ObjectToOBJECTREF((Object*)helperFtn(OBJECTREFToObject(chainedContinuation), pContinuationType));
                    }
                    break;
                }

                case INTOP_CAPTURE_CONTEXT_ON_SUSPEND:
                {
                    CONTINUATIONREF continuation = LOCAL_VAR(ip[2], CONTINUATIONREF);

                    if (continuation == NULL)
                    {
                        LOCAL_VAR(ip[1], CONTINUATIONREF) = continuation;
                        // No continuation to handle.
                        ip += 4;
                        break;
                    }

                    InterpAsyncSuspendData *pAsyncSuspendData = (InterpAsyncSuspendData*)pMethod->pDataItems[ip[3]];

                    THREADBASEREF threadBase = ((THREADBASEREF)GetThread()->GetExposedObject());
                    continuation = LOCAL_VAR(ip[2], CONTINUATIONREF);
                    OBJECTREF executionContext = threadBase->GetExecutionContext();
                    if (executionContext != NULL && ((EXECUTIONCONTEXTREF)executionContext)->IsFlowSuppressed())
                    {
                        executionContext = NULL;
                    }
                    SetObjectReference((OBJECTREF *)((uint8_t*)(OBJECTREFToObject(continuation)) + pAsyncSuspendData->offsetIntoContinuationTypeForExecutionContext), executionContext);
                    continuation->SetFlags(pAsyncSuspendData->flags);

                    if (pAsyncSuspendData->flags & CORINFO_CONTINUATION_HAS_CONTINUATION_CONTEXT)
                    {
                        MethodDesc *captureSyncContextMethod = pAsyncSuspendData->captureSyncContextMethod;
                        int32_t *flagsAddress = continuation->GetFlagsAddress();
                        size_t continuationOffset = OFFSETOF__CORINFO_Continuation__data;
                        uint8_t *pContinuationData = (uint8_t*)OBJECTREFToObject(continuation) + continuationOffset;
                        if (pAsyncSuspendData->flags & CORINFO_CONTINUATION_HAS_EXCEPTION)
                        {
                            pContinuationData += sizeof(OBJECTREF);
                        }

                        returnOffset = ip[1];
                        callArgsOffset = pMethod->allocaSize;

                        // Pass argument to the target method
                        LOCAL_VAR(callArgsOffset, void*) = pContinuationData;
                        LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, int32_t*) = flagsAddress;
                        targetMethod = captureSyncContextMethod;
                        ip += 4;
                        goto CALL_INTERP_METHOD;
                    }
                    else
                    {
                        ip += 4;
                        break;
                    }
                }

                case INTOP_RESTORE_CONTEXTS_ON_SUSPEND:
                {
                    CONTINUATIONREF newContinuation = LOCAL_VAR(ip[2], CONTINUATIONREF);
                    CONTINUATIONREF continuationArg = LOCAL_VAR(ip[3], CONTINUATIONREF);

                    int32_t resumed = continuationArg != NULL;
                    if ((newContinuation == NULL) || resumed)
                    {
                        LOCAL_VAR(ip[1], CONTINUATIONREF) = newContinuation;
                        // No continuation to handle.
                        ip += 6;
                        break;
                    }

                    OBJECTREF executionContext = LOCAL_VAR(ip[4], OBJECTREF);
                    OBJECTREF syncContext = LOCAL_VAR(ip[4] + INTERP_STACK_SLOT_SIZE, OBJECTREF);

                    InterpAsyncSuspendData *pAsyncSuspendData = (InterpAsyncSuspendData*)pMethod->pDataItems[ip[5]];
                    MethodDesc *restoreContextsMethod = pAsyncSuspendData->restoreContextsMethod;

                    returnOffset = ip[1];
                    callArgsOffset = pMethod->allocaSize;
                    // Pass argument to the target method
                    LOCAL_VAR(callArgsOffset, int32_t) = resumed;
                    LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE, OBJECTREF) = executionContext;
                    LOCAL_VAR(callArgsOffset + INTERP_STACK_SLOT_SIZE * 2, OBJECTREF) = syncContext;
                    targetMethod = restoreContextsMethod;
                    ip += 6;
                    goto CALL_INTERP_METHOD;
                }

                case INTOP_GET_CONTINUATION:
                {
                    LOCAL_VAR(ip[1], OBJECTREF) = pInterpreterFrame->GetContinuation();
                    pInterpreterFrame->SetContinuation(NULL);
                    ip += 2;
                    break;
                }

                case INTOP_SET_CONTINUATION:
                {
                    pInterpreterFrame->SetContinuation(LOCAL_VAR(ip[1], CONTINUATIONREF));
                    ip += 2;
                    break;
                }

                case INTOP_SET_CONTINUATION_NULL:
                {
                    pInterpreterFrame->SetContinuation(NULL);
                    ip += 1;
                    break;
                }

                case INTOP_HANDLE_CONTINUATION_SUSPEND:
                {
                    InterpAsyncSuspendData *pAsyncSuspendData = (InterpAsyncSuspendData*)pMethod->pDataItems[ip[2]];
                    CONTINUATIONREF continuation = LOCAL_VAR(ip[1], CONTINUATIONREF);

                    if (continuation == NULL)
                    {
                        // No continuation to handle.
                        ip += 3 + 2; // (3 for this opcode + 2 for the continuation resume which follows)
                        break;
                    }
                    ip += 3;
                    
                    // copy locals that need to move to the continuation object
                    size_t continuationOffset = OFFSETOF__CORINFO_Continuation__data;
                    uint8_t *pContinuationDataStart = continuation->GetResultStorage();
                    uint8_t *pContinuationData = pContinuationDataStart;
                    size_t bytesTotal = 0;
                    InterpIntervalMapEntry *pCopyEntry = pAsyncSuspendData->liveLocalsIntervals;
                    if (pCopyEntry->countBytes > 0)
                    {
                        GCHeapMemoryBarrier();
                        while (pCopyEntry->countBytes != 0)
                        {
                            InlinedForwardGCSafeCopyHelper(pContinuationData, LOCAL_VAR_ADDR(pCopyEntry->startOffset, uint8_t), pCopyEntry->countBytes);
                            bytesTotal += pCopyEntry->countBytes;
                            pContinuationData += pCopyEntry->countBytes;
                            pCopyEntry++;
                        }
                        InlinedSetCardsAfterBulkCopyHelper((Object**)pContinuationDataStart, bytesTotal);
                    }
                    
                    int32_t returnValueSize = pAsyncSuspendData->asyncMethodReturnTypePrimitiveSize;
                    if (pAsyncSuspendData->asyncMethodReturnType != NULL)
                    {
                        if (pAsyncSuspendData->asyncMethodReturnType->IsValueType())
                            returnValueSize = pAsyncSuspendData->asyncMethodReturnType->GetNumInstanceFieldBytes();
                        else
                            returnValueSize = sizeof(OBJECTREF);
                    }

                    if (returnValueSize > 0)
                    {
                        if (returnValueSize > INTERP_STACK_SLOT_SIZE)
                        {
                            memset(pFrame->pRetVal, 0, returnValueSize);
                        }
                        else
                        {
                            *(int64_t*)pFrame->pRetVal = 0;
                        }
                    }

                    continuation->SetState((int32_t)((uint8_t*)ip - (uint8_t*)pFrame->startIp));
                    _ASSERTE(pAsyncSuspendData->methodStartIP != 0);
                    continuation->SetResumeInfo(&pAsyncSuspendData->resumeInfo);
                    pInterpreterFrame->SetContinuation(continuation);
                    goto EXIT_FRAME;
                }

                case INTOP_HANDLE_CONTINUATION_RESUME:
                {
                    InterpAsyncSuspendData *pAsyncSuspendData = (InterpAsyncSuspendData*)pMethod->pDataItems[ip[1]];
                    CONTINUATIONREF continuation = (CONTINUATIONREF)ObjectToOBJECTREF(*(Object**)(stack + pAsyncSuspendData->continuationArgOffset));
                    _ASSERTE(pInterpreterFrame->GetContinuation() == NULL);

                    // The INTOP_CHECK_FOR_CONTINUATION opcode will have called to restore the execution context already
                    // Now copy the locals

                    // copy locals that need to move from the continuation object
                    uint8_t *pContinuationData = continuation->GetResultStorage();
                    InterpIntervalMapEntry *pCopyEntry = pAsyncSuspendData->liveLocalsIntervals;
                    while (pCopyEntry->countBytes != 0)
                    {
                        memcpy(LOCAL_VAR_ADDR(pCopyEntry->startOffset, uint8_t), pContinuationData, pCopyEntry->countBytes);
                        pContinuationData += pCopyEntry->countBytes;
                        pCopyEntry++;
                    }

                    if (pAsyncSuspendData->flags & CORINFO_CONTINUATION_HAS_EXCEPTION)
                    {
                        // Throw exception if needed
                        OBJECTREF exception = *continuation->GetExceptionObjectStorage();

                        if (exception != NULL)
                        {
                            GetThread()->GetExceptionState()->SetRaisingForeignException();
                            pInterpreterFrame->SetIsFaulting(true);
                            DispatchManagedException(exception);
                            UNREACHABLE();
                        }
                    }

                    ip += 2;
                    break;
                }

                case INTOP_CHECK_FOR_CONTINUATION:
                {
                    CONTINUATIONREF continuation = LOCAL_VAR(ip[1], CONTINUATIONREF);
                    _ASSERTE(pInterpreterFrame->GetContinuation() == NULL);
                    if (continuation != NULL)
                    {
                        // A continuation is present, begin the restoration process
                        int32_t state = continuation->GetState();
                        ip = (int32_t*)((uint8_t*)pFrame->startIp + state);
                        
                        // Now we have an IP to where we should resume execution. This should be an INTOP_HANDLE_CONTINUATION_RESUME opcode
                        // And before it should be an INTOP_HANDLE_CONTINUATION_SUSPEND opcode
                        _ASSERTE(*ip == INTOP_HANDLE_CONTINUATION_RESUME);
                        _ASSERTE(*(ip-3) == INTOP_HANDLE_CONTINUATION_SUSPEND);
                        InterpAsyncSuspendData *pAsyncSuspendData = (InterpAsyncSuspendData*)pMethod->pDataItems[ip[1]];

                        returnOffset = pMethod->allocaSize;
                        callArgsOffset = pMethod->allocaSize;

                        OBJECTREF execContext = ObjectToOBJECTREF(*(Object**)(((uint8_t*)OBJECTREFToObject(continuation)) + pAsyncSuspendData->offsetIntoContinuationTypeForExecutionContext));

                        // We need to call the RestoreExecutionContext helper method
                        LOCAL_VAR(callArgsOffset, OBJECTREF) = execContext;

                        targetMethod = pAsyncSuspendData->restoreExecutionContextMethod;
                        goto CALL_INTERP_METHOD;
                    }
                    ip += 3;
                    break;
                }
                default:
                    EEPOLICY_HANDLE_FATAL_ERROR_WITH_MESSAGE(COR_E_EXECUTIONENGINE, W("Unimplemented or invalid interpreter opcode\n"));
                    break;
            }
        }
        UNINSTALL_UNWIND_AND_CONTINUE_HANDLER;
        UNINSTALL_MANAGED_EXCEPTION_DISPATCHER;
    }
    catch (const ResumeAfterCatchException& ex)
    {
        TADDR resumeSP;
        TADDR resumeIP;
        ex.GetResumeContext(&resumeSP, &resumeIP);
        _ASSERTE(resumeSP != 0 && resumeIP != 0);

        InterpMethodContextFrame* pResumeFrame = (InterpMethodContextFrame*)resumeSP;
        // Unwind the interpreter stack upto the resume frame
        while (pFrame != pResumeFrame)
        {
            if (pFrame == NULL)
            {
                // In scenarios when the interpreted code ends up calling other interpreted code through the precode, there are two separate
                // sequences of interpreted frames without any AOTed/JITted frames in between. In such case, the topmost native frame
                // the ResumeAfterCatchException is thrown from may not be the one that corresponds to the target interpreted frame.
                // Thus, we need to rethrow it to let it propagate further.
                throw;
            }
            pThreadContext->frameDataAllocator.PopInfo(pFrame);
            pFrame->ip = 0;
            pFrame = pFrame->pParent;
        }

        // Set the current interpreter context to the resume one and continue execution from there
        ip = (int32_t*)resumeIP;

        stack = pFrame->pStack;
        pMethod = pFrame->startIp->Method;
        _ASSERTE(pMethod->CheckIntegrity());

        pThreadContext->pStackPointer = pFrame->pStack + pMethod->allocaSize;

        pInterpreterFrame->SetIsFaulting(false);

        Thread *pThread = GetThread();
        if (pThread->IsAbortRequested())
        {
            // Record the resume IP in the pFrame so that the exception handling unwinds from there
            pFrame->ip = ip;
            DispatchManagedException(kThreadAbortException);
        }

        goto MAIN_LOOP;
    }

EXIT_FRAME:

    // Exit the current frame, MAKE CERTAIN not to trigger any GC between here and the return, since the interpreter
    // async resumption logic depends on not triggering a GC here for correctness.

    // Interpreter-TODO: Don't run PopInfo on the main return path, Add RET_LOCALLOC instead
    pThreadContext->frameDataAllocator.PopInfo(pFrame);
    if (pFrame->pParent && pFrame->pParent->ip)
    {
        // Return to the main loop after a non-recursive interpreter call
        pFrame->ip = NULL;
        pFrame = pFrame->pParent;
        ip = pFrame->ip;
        stack = pFrame->pStack;
        pMethod = pFrame->startIp->Method;
        _ASSERTE(pMethod->CheckIntegrity());
        pFrame->ip = NULL;

        pThreadContext->pStackPointer = pFrame->pStack + pMethod->allocaSize;
        goto MAIN_LOOP;
    }

    pThreadContext->pStackPointer = pFrame->pStack;
}

#endif // FEATURE_INTERPRETER
